From 64fd28d40499ee9911e11bf49157a4aea80b865b Mon Sep 17 00:00:00 2001 From: Sirac Ozmen Date: Thu, 5 Nov 2026 14:19:58 +0300 Subject: [PATCH 1/2] build(deps)!: move Maul2D to 1.3.1 Vendor Maul2D 1.3.1 from its release archive and follow its renames, reordered list queries and new statuses. The 2D step refuses a tick Maul2D cannot step before it changes anything, and the statistics count only kinematic targets carried out. BREAKING CHANGE: the engine is built on Maul2D 1.3.1. The 2D physics digest changes for every world, since Maul2D's world hash now covers each shape's broadphase box and each body's sleep threshold; body poses are unchanged. Queries between steps see a shape that moved more than 0.1 m in the last step where it is now. A body with a locked rotation no longer turns when given a spin. The 2D step fails at a tick shorter than 1 microsecond or longer than 10 seconds, and the statistics count only kinematic targets carried out. --- docs/internals/physics-and-navigation.md | 2 + docs/operations/dependencies.md | 2 +- docs/user/dedicated-server.md | 2 +- .../include/rawframe/physics2d/errors.h | 4 +- modules/physics2d/src/characters.cpp | 6 +- modules/physics2d/src/joints.cpp | 4 +- modules/physics2d/src/physics.cpp | 35 +- modules/physics2d/tests/physics_test.cpp | 23 +- third_party/maul2d/CMakeLists.txt | 1 + third_party/maul2d/include/maul2d/base.h | 31 +- third_party/maul2d/include/maul2d/body.h | 83 ++-- third_party/maul2d/include/maul2d/core_math.h | 95 +---- third_party/maul2d/include/maul2d/events.h | 20 +- third_party/maul2d/include/maul2d/joint.h | 124 ++++-- third_party/maul2d/include/maul2d/particle.h | 56 +-- third_party/maul2d/include/maul2d/shape.h | 272 +++++++------ third_party/maul2d/include/maul2d/world.h | 266 ++++++++----- third_party/maul2d/src/body.c | 65 ++- third_party/maul2d/src/body.h | 3 +- third_party/maul2d/src/body_motion.c | 39 +- third_party/maul2d/src/body_read.c | 31 +- third_party/maul2d/src/broadphase.c | 95 +++-- third_party/maul2d/src/broadphase.h | 21 + third_party/maul2d/src/buoyancy.c | 221 ++++++----- third_party/maul2d/src/buoyancy.h | 19 +- third_party/maul2d/src/ccd.c | 84 ++-- third_party/maul2d/src/chain.c | 66 +++- third_party/maul2d/src/contact.c | 131 +++---- third_party/maul2d/src/contact.h | 10 + third_party/maul2d/src/contact_solver.c | 4 +- third_party/maul2d/src/core.c | 2 +- third_party/maul2d/src/core.h | 158 ++++++-- third_party/maul2d/src/cpu.c | 18 +- third_party/maul2d/src/cpu.h | 23 +- third_party/maul2d/src/decompose.c | 161 +++++++- third_party/maul2d/src/destruction.c | 35 +- third_party/maul2d/src/distance.c | 2 +- third_party/maul2d/src/dynamic_tree.c | 38 -- third_party/maul2d/src/dynamic_tree.h | 10 +- third_party/maul2d/src/format_layout.c | 65 +-- third_party/maul2d/src/geometry.c | 89 ++--- third_party/maul2d/src/geometry.h | 15 +- third_party/maul2d/src/hash.c | 4 +- third_party/maul2d/src/island.c | 19 +- third_party/maul2d/src/island.h | 9 + third_party/maul2d/src/joint.c | 38 +- third_party/maul2d/src/joint.h | 7 +- third_party/maul2d/src/joint_distance.c | 4 +- third_party/maul2d/src/joint_edges.h | 2 +- third_party/maul2d/src/joint_gear.c | 2 +- third_party/maul2d/src/joint_motor.c | 45 +++ third_party/maul2d/src/joint_mouse.c | 8 +- third_party/maul2d/src/joint_prismatic.c | 6 +- third_party/maul2d/src/joint_pulley.c | 2 +- third_party/maul2d/src/joint_ratchet.c | 16 +- third_party/maul2d/src/joint_read.c | 6 +- third_party/maul2d/src/joint_revolute.c | 6 +- third_party/maul2d/src/joint_row.c | 2 +- third_party/maul2d/src/joint_row.h | 5 +- third_party/maul2d/src/joint_solver.c | 4 +- third_party/maul2d/src/joint_wheel.c | 6 +- third_party/maul2d/src/journal.c | 162 +++++++- third_party/maul2d/src/journal.h | 42 +- third_party/maul2d/src/journal_replay.c | 146 +++---- third_party/maul2d/src/manifold.c | 6 +- third_party/maul2d/src/mass.h | 2 +- third_party/maul2d/src/math_functions.c | 10 +- third_party/maul2d/src/math_functions.h | 7 +- third_party/maul2d/src/mover.c | 116 ++++-- third_party/maul2d/src/parallel.c | 5 +- third_party/maul2d/src/parallel.h | 3 +- third_party/maul2d/src/particle.c | 127 +++--- third_party/maul2d/src/particle_contacts.c | 131 ++++--- third_party/maul2d/src/particle_pairs.c | 4 +- third_party/maul2d/src/proxies.h | 2 +- third_party/maul2d/src/query.c | 134 +++---- third_party/maul2d/src/query.h | 31 +- third_party/maul2d/src/ray_cast.c | 2 +- third_party/maul2d/src/rotation.h | 8 +- third_party/maul2d/src/shape.c | 42 +- third_party/maul2d/src/shape.h | 3 +- third_party/maul2d/src/shape_read.c | 11 +- third_party/maul2d/src/snapshot.c | 49 ++- third_party/maul2d/src/snapshot.h | 2 +- third_party/maul2d/src/snapshot_check.c | 280 ++++++------- third_party/maul2d/src/snapshot_check.h | 5 +- third_party/maul2d/src/snapshot_pairs.c | 369 ++++++++++++++++++ third_party/maul2d/src/snapshot_pairs.h | 23 ++ third_party/maul2d/src/snapshot_read.h | 70 ++++ third_party/maul2d/src/solver.c | 13 +- third_party/maul2d/src/state_checks.c | 33 +- third_party/maul2d/src/state_checks.h | 31 +- third_party/maul2d/src/world.c | 176 +++++---- third_party/maul2d/src/world_hash.c | 15 +- third_party/maul2d/src/world_hash.h | 2 +- third_party/maul2d/src/world_internal.c | 44 ++- third_party/maul2d/src/world_internal.h | 111 ++++-- third_party/maul2d/src/world_state.c | 82 ++-- third_party/maul2d/src/world_state.h | 8 +- third_party/maul2d/src/world_validate.c | 16 +- 100 files changed, 3194 insertions(+), 1751 deletions(-) create mode 100644 third_party/maul2d/src/snapshot_pairs.c create mode 100644 third_party/maul2d/src/snapshot_pairs.h create mode 100644 third_party/maul2d/src/snapshot_read.h diff --git a/docs/internals/physics-and-navigation.md b/docs/internals/physics-and-navigation.md index 9cb1ed1bf..5a00da050 100644 --- a/docs/internals/physics-and-navigation.md +++ b/docs/internals/physics-and-navigation.md @@ -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 diff --git a/docs/operations/dependencies.md b/docs/operations/dependencies.md index 6a55d2b1f..60c5de0f9 100644 --- a/docs/operations/dependencies.md +++ b/docs/operations/dependencies.md @@ -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` | diff --git a/docs/user/dedicated-server.md b/docs/user/dedicated-server.md index a82775b2f..2fe840b07 100644 --- a/docs/user/dedicated-server.md +++ b/docs/user/dedicated-server.md @@ -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. | diff --git a/modules/physics2d/include/rawframe/physics2d/errors.h b/modules/physics2d/include/rawframe/physics2d/errors.h index f9539089d..5db805bf7 100644 --- a/modules/physics2d/include/rawframe/physics2d/errors.h +++ b/modules/physics2d/include/rawframe/physics2d/errors.h @@ -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, diff --git a/modules/physics2d/src/characters.cpp b/modules/physics2d/src/characters.cpp index f1c885105..913781eae 100644 --- a/modules/physics2d/src/characters.cpp +++ b/modules/physics2d/src/characters.cpp @@ -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 planes{}; + std::array 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(by.x); diff --git a/modules/physics2d/src/joints.cpp b/modules/physics2d/src/joints.cpp index 15e2019c2..e0b496756 100644 --- a/modules/physics2d/src/joints.cpp +++ b/modules/physics2d/src/joints.cpp @@ -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{}; } @@ -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{}; } diff --git a/modules/physics2d/src/physics.cpp b/modules/physics2d/src/physics.cpp index 8e014a5d9..cc2f16225 100644 --- a/modules/physics2d/src/physics.cpp +++ b/modules/physics2d/src/physics.cpp @@ -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" @@ -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) { @@ -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(static_cast(rate.seconds) / static_cast(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) { @@ -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(static_cast(rate.seconds) / static_cast(rate.ticks)); for (const Row& row : rows) { const auto [kEntry, kNew] = mapped.try_emplace(row.entity); Mapped& entry = kEntry->second; @@ -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(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; } @@ -709,7 +721,10 @@ struct Physics2D::State { moveCharacter(row, mapped.find(row.entity)->second, kSeconds); } } - m2World_Step(physics, kSeconds, static_cast(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(settings.substeps)); + RAWFRAME_CHECK(kStepped == m2_success, "Maul2D refused a step whose length and substeps were checked"); ++statistics.steps; report(world); @@ -784,7 +799,7 @@ result::Result> 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"); @@ -898,10 +913,10 @@ void Physics2D::overlapCircle( std::vector& shapes = state_->overlapShapes; shapes.resize(std::max(shapes.size(), 16)); std::int32_t total = m2World_OverlapCircle( - state_->physics, &kCircle, kAt, shapes.data(), static_cast(shapes.size()), *kFilter); + state_->physics, &kCircle, kAt, *kFilter, shapes.data(), static_cast(shapes.size())); if (static_cast(total) > shapes.size()) { shapes.resize(static_cast(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(index)]); diff --git a/modules/physics2d/tests/physics_test.cpp b/modules/physics2d/tests/physics_test.cpp index fed99cdeb..b6f273418 100644 --- a/modules/physics2d/tests/physics_test.cpp +++ b/modules/physics2d/tests/physics_test.cpp @@ -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); } @@ -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. @@ -531,6 +536,22 @@ 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(); diff --git a/third_party/maul2d/CMakeLists.txt b/third_party/maul2d/CMakeLists.txt index 97e05c602..94d412c38 100644 --- a/third_party/maul2d/CMakeLists.txt +++ b/third_party/maul2d/CMakeLists.txt @@ -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 diff --git a/third_party/maul2d/include/maul2d/base.h b/third_party/maul2d/include/maul2d/base.h index f1f1bddab..458819fdf 100644 --- a/third_party/maul2d/include/maul2d/base.h +++ b/third_party/maul2d/include/maul2d/base.h @@ -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, @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/third_party/maul2d/include/maul2d/body.h b/third_party/maul2d/include/maul2d/body.h index e9144aabc..6118fecac 100644 --- a/third_party/maul2d/include/maul2d/body.h +++ b/third_party/maul2d/include/maul2d/body.h @@ -14,20 +14,17 @@ extern "C" { #endif - /// A box in world space, its corners 64-bit positions: what a body or - /// shape covers, and the region a box query looks in. - typedef struct m2WorldAabb - { - m2Pos2 lowerBound; - m2Pos2 upperBound; - } m2WorldAabb; + /// How a body moves: one of the m2_*Body constants. The type has a + /// fixed width, unlike a C enum's, so a def holding it has one layout + /// in every compiler, C and C++. + typedef int32_t m2BodyType; - typedef enum m2BodyType + enum { m2_staticBody = 0, m2_kinematicBody = 1, m2_dynamicBody = 2, - } m2BodyType; + }; /// A body of a world: a 1-based slot and a generation. typedef struct m2BodyId @@ -61,11 +58,12 @@ extern "C" float angularDamping; m2MotionLocks motionLocks; // angularZ: never rotates, infinite inertia bool enableSleep; // false: this body never sleeps - bool isEnabled; // false: created dormant, outside the simulation + bool enable; // false: created dormant, outside the simulation int8_t dominance; // the higher side of a contact cannot be pushed - bool isBullet; // continuous collision against non-bullets + bool enableBullet; // continuous collision against non-bullets M2_ALIGN8 uint64_t userData; // opaque, kept through snapshots and journals int32_t internalValue; // the cookie m2DefaultBodyDef sets + float sleepThreshold; // resting speed, m/s; 0 = the default 0.05 } m2BodyDef; /// Returns a body def: a static, enabled body at the origin with @@ -86,7 +84,8 @@ extern "C" /// zero for a static body; its rotation not zero, and /// normalized to its direction; its gravity scale /// within M2_GRAVITY_SCALE_LIMIT; its dampings from 0 - /// to M2_RATE_LIMIT. + /// to M2_RATE_LIMIT; its sleep threshold from 0 to + /// M2_SPEED_LIMIT. /// @param bodyOut Receives the body; the null id on failure. /// @return m2_success; m2_errorInvalid for a null argument, a def not /// from m2DefaultBodyDef, a NaN or a field outside its domain, @@ -106,7 +105,8 @@ extern "C" /// /// @param world The world. /// @param bodyId The body. - /// @return m2_success; m2_errorStale when the body is already gone; + /// @return m2_success; m2_errorInvalid for a null world; + /// m2_errorStale when the body is already gone; /// m2_errorBusy from host code the world is running. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. @@ -214,7 +214,7 @@ extern "C" /// @param flag True for a bullet. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. - M2_API void m2Body_SetBullet(m2World* world, m2BodyId bodyId, bool flag); + M2_API void m2Body_EnableBullet(m2World* world, m2BodyId bodyId, bool flag); /// Sets a body's opaque game data, kept through snapshots and journals. /// A stale id counts one misuse. Journaled. @@ -250,8 +250,7 @@ extern "C" /// Sets the velocities that carry a body to a target pose in one step /// of the given length: the way to drive a kinematic body. It goes /// through the journaled velocity setters, so a replay repeats it. A - /// stale id, a static body, an invalid argument or a pose too far to - /// reach within M2_SPEED_LIMIT changes nothing and counts one misuse. + /// refused call changes nothing. /// /// @param world The world. /// @param bodyId The body. @@ -259,10 +258,19 @@ extern "C" /// @param rotation The target rotation, unit length. /// @param dt The step the move takes, from M2_MIN_STEP to /// M2_STEP_LIMIT. + /// @return m2_success; m2_errorInvalid for a null world, a static body, + /// a position that is not finite, a rotation that is not unit + /// length or a dt of 0 or below; m2_errorStale for a stale id; + /// m2_errorLimit for a dt outside M2_MIN_STEP to M2_STEP_LIMIT, + /// and for a pose too far from the body to reach within + /// M2_SPEED_LIMIT, which counts no misuse, since where the body + /// is is the world's state; m2_errorBusy from host code the + /// world is running. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. - M2_API void m2Body_SetTargetTransform(m2World* world, m2BodyId bodyId, m2Pos2 position, - m2Rot rotation, float dt); + M2_NODISCARD M2_API m2Result m2Body_SetTargetTransform(m2World* world, m2BodyId bodyId, + m2Pos2 position, m2Rot rotation, + float dt); /// Reads a body's type. /// @@ -332,7 +340,7 @@ extern "C" /// @param flag True to enable it, false to disable it. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. - M2_API void m2Body_SetEnabled(m2World* world, m2BodyId bodyId, bool flag); + M2_API void m2Body_Enable(m2World* world, m2BodyId bodyId, bool flag); /// Tells whether a body takes part in the simulation. /// @@ -362,7 +370,7 @@ extern "C" /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API bool m2Body_IsBullet(const m2World* world, m2BodyId bodyId); + M2_API bool m2Body_IsBulletEnabled(const m2World* world, m2BodyId bodyId); /// Reads a body's gravity scale. /// @@ -377,14 +385,14 @@ extern "C" /// Lists the world's bodies in ascending slot order. /// /// @param world The world. - /// @param ids Receives up to capacity bodies; may be null to - /// measure. - /// @param capacity The length of ids. + /// @param idsOut Receives up to capacity bodies; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of idsOut, not below 0. /// @return The number of bodies, which may exceed capacity. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API int32_t m2World_GetBodies(const m2World* world, m2BodyId* ids, int32_t capacity); + M2_API int32_t m2World_GetBodies(const m2World* world, m2BodyId* idsOut, int32_t capacity); /// Maps a point from a body's frame to world space. /// @@ -701,6 +709,33 @@ extern "C" /// several readers at once. M2_API bool m2Body_IsSleepEnabled(const m2World* world, m2BodyId bodyId); + /// Sets the speed below which a body counts as resting. A body rests + /// while its speed is below the threshold t and its spin below + /// 0.12 rad/s times t / 0.05; resting for half a second with the rest + /// of its island, it falls asleep. 0 gives the default, 0.05 m/s and so + /// 0.12 rad/s. A kinematic body below its threshold does not wake what + /// it touches. The change applies from the next step: a sleeping body + /// stays asleep. A stale id, or a threshold outside 0 to + /// M2_SPEED_LIMIT, changes nothing and counts one misuse. Journaled. + /// + /// @param world The world. + /// @param bodyId The body. + /// @param threshold The resting speed, m/s; 0 for the default. + /// @par Thread safety + /// Safe from any thread; a world is used by one thread at a time. + M2_API void m2Body_SetSleepThreshold(m2World* world, m2BodyId bodyId, float threshold); + + /// Reads the speed below which a body counts as resting. + /// + /// @param world The world. + /// @param bodyId The body; a stale id counts one misuse. + /// @return The threshold, m/s: 0.05 for a body given the default; + /// 0 for a stale id. + /// @par Thread safety + /// Safe from any thread; a world is used by one thread at a time, or by + /// several readers at once. + M2_API float m2Body_GetSleepThreshold(const m2World* world, m2BodyId bodyId); + /// Applies an angular impulse, changing the angular velocity at once. /// The body wakes, and its spin saturates at M2_SPEED_LIMIT. A stale id, a /// body that is not dynamic or an impulse beyond M2_IMPULSE_LIMIT diff --git a/third_party/maul2d/include/maul2d/core_math.h b/third_party/maul2d/include/maul2d/core_math.h index 445ea35fd..8d55d5e16 100644 --- a/third_party/maul2d/include/maul2d/core_math.h +++ b/third_party/maul2d/include/maul2d/core_math.h @@ -11,8 +11,6 @@ #include "maul2d/base.h" -#include - #ifdef __cplusplus extern "C" { @@ -50,96 +48,19 @@ extern "C" m2Rot q; } m2Transform; -#if defined(_MSC_VER) && defined(_M_X64) -#include -#elif defined(_MSC_VER) && defined(_M_ARM64) -#include -#endif - - /// Pinned minimum: exactly (a < b ? a : b), in this operand order - - /// including signed zero and NaN behavior. MSVC folds the plain ternary - /// under value numbering (+0/-0 compare equal), so on MSVC x64 this is - /// implemented with scalar MINSS, whose ISA-defined semantics are exactly - /// the pinned form. NEON FMIN has different semantics and must never be - /// used here; GCC/Clang compile the ternary faithfully (CI-verified). - static inline float m2MinF(float a, float b) + /// A box in world space, its corners 64-bit positions: what a body or + /// shape covers, the region a box query looks in, and a water volume's + /// region. + typedef struct m2WorldAabb { -#if defined(_MSC_VER) && defined(_M_X64) - return _mm_cvtss_f32(_mm_min_ss(_mm_set_ss(a), _mm_set_ss(b))); -#elif defined(_MSC_VER) && defined(_M_ARM64) - // The plain ternary got folded away from the pinned zero-sign - // semantics (the arm64 cell caught it on its first run), so the - // select is explicit NEON: compare into a mask, bit-select the - // lanes - never fminnm. - float32x2_t va = vdup_n_f32(a); - float32x2_t vb = vdup_n_f32(b); - return vget_lane_f32(vbsl_f32(vclt_f32(va, vb), va, vb), 0); -#elif defined(_MSC_VER) -#error \ - "MSVC on this target: pinned min/max semantics unproven; implement with verified intrinsics first" -#else - return a < b ? a : b; -#endif - } - - /// Pinned maximum: exactly (a > b ? a : b), in this operand order. - /// See m2MinF for the MSVC note (scalar MAXSS matches the pinned form). - static inline float m2MaxF(float a, float b) - { -#if defined(_MSC_VER) && defined(_M_X64) - return _mm_cvtss_f32(_mm_max_ss(_mm_set_ss(a), _mm_set_ss(b))); -#elif defined(_MSC_VER) && defined(_M_ARM64) - // See m2MinF: explicit NEON compare + bit-select. - float32x2_t va = vdup_n_f32(a); - float32x2_t vb = vdup_n_f32(b); - return vget_lane_f32(vbsl_f32(vcgt_f32(va, vb), va, vb), 0); -#elif defined(_MSC_VER) -#error \ - "MSVC on this target: pinned min/max semantics unproven; implement with verified intrinsics first" -#else - return a > b ? a : b; -#endif - } - - /// Absolute value, pinned to IEEE |x| semantics: the sign bit is - /// cleared, so m2AbsF(-0.0f) == +0.0f on every platform. Implemented - /// as a bit mask because a ternary abs is compiler-foldable into - /// divergent ±0 behavior (MSVC emits a sign-mask, GCC/Clang keep the - /// branch - a real cross-cell hash break, caught by the gate). - static inline float m2AbsF(float a) - { - union - { - float f; - uint32_t u; - } bits; - bits.f = a; - bits.u &= 0x7FFFFFFFu; - return bits.f; - } - - /// Clamp to [lo, hi] using the pinned min/max. - static inline float m2ClampF(float a, float lo, float hi) - { - return m2MaxF(lo, m2MinF(a, hi)); - } - - /// Maps an angle to [-pi, pi], using only +, -, *, / and floorf, so the - /// bits agree everywhere. The boundary lands within one rounding step - /// of pi. - /// - /// @param radians Any angle. Beyond about 1.0e6 in magnitude a float - /// cannot fold precisely; such an angle returns the - /// boundary, never NaN. - /// @return The same angle in [-pi, pi]. - /// @par Thread safety - /// Safe from any thread. - M2_API float m2UnwindAngle(float radians); + m2Pos2 lowerBound; + m2Pos2 upperBound; + } m2WorldAabb; /// Builds a rotation from an angle without calling the C library, so the /// bits agree everywhere. Accurate to about 4e-7. /// - /// @param radians Any angle; it is unwound first (m2UnwindAngle), so an + /// @param radians Any angle; it is mapped into [-pi, pi] first, so an /// out-of-range or non-finite angle never yields NaN. /// @return A unit rotation. /// @par Thread safety diff --git a/third_party/maul2d/include/maul2d/events.h b/third_party/maul2d/include/maul2d/events.h index b3b4aa045..e320dc62f 100644 --- a/third_party/maul2d/include/maul2d/events.h +++ b/third_party/maul2d/include/maul2d/events.h @@ -134,17 +134,17 @@ extern "C" /// Lists the shapes inside a sensor now, in canonical pair order. /// /// @param world The world. - /// @param sensorShapeId The sensor. A stale id counts one misuse; a - /// live shape that is not a sensor lists nothing. - /// @param overlaps Receives up to capacity shape ids; may be null - /// to measure. - /// @param capacity The length of overlaps. + /// @param sensorShapeId The sensor. A stale id or a shape that is not a + /// sensor counts one misuse. + /// @param overlapsOut Receives up to capacity shape ids; zeros over the whole capacity on + /// failure. May be null with a capacity of 0 to measure. + /// @param capacity The length of overlapsOut, not below 0. /// @return The number of shapes inside, which may exceed capacity. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. M2_API int32_t m2Shape_GetSensorOverlaps(const m2World* world, m2ShapeId sensorShapeId, - m2ShapeId* overlaps, int32_t capacity); + m2ShapeId* overlapsOut, int32_t capacity); /// One contact touching now, as the solver last saw it. typedef struct m2ContactData @@ -162,14 +162,14 @@ extern "C" /// Sensor overlaps are not contacts and are left out. /// /// @param world The world. - /// @param data Receives up to capacity contacts; may be null to - /// measure. - /// @param capacity The length of data. + /// @param dataOut Receives up to capacity contacts; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of dataOut, not below 0. /// @return The number of touching contacts, which may exceed capacity. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API int32_t m2World_GetContactData(const m2World* world, m2ContactData* data, + M2_API int32_t m2World_GetContactData(const m2World* world, m2ContactData* dataOut, int32_t capacity); #ifdef __cplusplus diff --git a/third_party/maul2d/include/maul2d/joint.h b/third_party/maul2d/include/maul2d/joint.h index 672b8136e..45bd41735 100644 --- a/third_party/maul2d/include/maul2d/joint.h +++ b/third_party/maul2d/include/maul2d/joint.h @@ -8,9 +8,9 @@ // counts one misuse. A call made for one joint type (a motor joint's // offsets, a distance joint's length) does the same on a joint of // another type. Motors and limits belong to the revolute, prismatic and -// wheel joints; m2Joint_SetMaxMotor also sets a motor joint's torque -// budget. Every setter is journaled, and one that changes how a joint -// pulls wakes the bodies it moves. +// wheel joints; m2Joint_SetMaxMotorEffort also sets a motor joint's torque +// budget, as m2MotorJoint_SetMaxTorque does. Every setter is journaled, and one that changes how a +// joint pulls wakes the bodies it moves. #ifndef MAUL2D_JOINT_H #define MAUL2D_JOINT_H @@ -22,7 +22,12 @@ extern "C" { #endif - typedef enum m2JointType + /// A joint's kind: one of the m2_*Joint constants. The type has a + /// fixed width, unlike a C enum's, so it crosses the interface the same + /// in every compiler, C and C++. + typedef int32_t m2JointType; + + enum { m2_distanceJoint = 0, m2_revoluteJoint = 1, @@ -35,7 +40,7 @@ extern "C" m2_gearJoint = 8, m2_pulleyJoint = 9, m2_ratchetJoint = 10, - } m2JointType; + }; /// A joint of a world: a 1-based slot and a generation. typedef struct m2JointId @@ -93,7 +98,7 @@ extern "C" bool enableSpring; // off by default; springHertz 0 is no spring force bool enableMotor; float motorSpeed; // rad/s, positive: B turns counter-clockwise against A - float maxMotorTorque; // N m + float maxMotorEffort; // the motor's torque budget, N m bool enableLimit; float lowerAngle; // radians from the creation angle float upperAngle; @@ -116,8 +121,8 @@ extern "C" float constraintHertz; // the slide's softness; 0: the stiff default float constraintDampingRatio; bool enableMotor; - float motorSpeed; // m/s along the axis - float maxMotorForce; // N + float motorSpeed; // m/s along the axis + float maxMotorEffort; // the motor's force budget, N bool enableLimit; float lowerTranslation; // meters from the separation at creation float upperTranslation; @@ -159,7 +164,7 @@ extern "C" float springDampingRatio; bool enableMotor; float motorSpeed; // rad/s of B against A - float maxMotorTorque; // N m + float maxMotorEffort; // the motor's torque budget, N m bool enableLimit; float lowerTranslation; // meters from the separation at creation float upperTranslation; @@ -254,7 +259,7 @@ extern "C" m2BodyId bodyIdB; /// The tooth angle, M2_MIN_RATCHET_TOOTH to M2_ANGLE_LIMIT by /// magnitude; its sign is the free direction. - float ratchet; + float toothAngle; float phase; // offsets the teeth, radians M2_ALIGN8 uint64_t userData; bool enableConnectedCollision; @@ -477,12 +482,16 @@ extern "C" M2_NODISCARD M2_API m2Result m2CreateMotorJoint(m2World* world, const m2MotorJointDef* def, m2JointId* jointOut); - /// Creates a mouse joint, as m2CreateDistanceJoint does. + /// Creates a mouse joint, as m2CreateDistanceJoint does. The grab + /// point, body B's anchor, is where the target lies in B's frame at + /// creation, so it lies within M2_LOCAL_LIMIT like any other anchor. /// /// @param world The world. /// @param def The joint, from m2DefaultMouseJointDef. /// @param jointOut Receives the joint; the null id on failure. - /// @return As m2CreateDistanceJoint. + /// @return As m2CreateDistanceJoint, and m2_errorLimit for a target past + /// M2_LOCAL_LIMIT from B in B's frame, which counts no misuse, + /// since where B is is the world's state. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. M2_NODISCARD M2_API m2Result m2CreateMouseJoint(m2World* world, const m2MouseJointDef* def, @@ -492,7 +501,8 @@ extern "C" /// /// @param world The world. /// @param jointId The joint. - /// @return m2_success; m2_errorStale when it is already gone; + /// @return m2_success; m2_errorInvalid for a null world; + /// m2_errorStale when it is already gone; /// m2_errorBusy from host code the world is running. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. @@ -513,12 +523,12 @@ extern "C" /// a motor joint, a force on a prismatic joint. A budget outside 0 to /// M2_FORCE_LIMIT or another joint type counts one misuse. /// - /// @param world The world. - /// @param jointId A revolute, prismatic, wheel or motor joint. - /// @param maxTorqueOrForce The budget, N m or N. + /// @param world The world. + /// @param jointId A revolute, prismatic, wheel or motor joint. + /// @param maxEffort The budget, N m or N. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. - M2_API void m2Joint_SetMaxMotor(m2World* world, m2JointId jointId, float maxTorqueOrForce); + M2_API void m2Joint_SetMaxMotorEffort(m2World* world, m2JointId jointId, float maxEffort); /// Turns a motor on or off. Another joint type counts one misuse. /// @@ -699,8 +709,10 @@ extern "C" /// @param world The world. /// @param jointId A distance joint; another type or a stale id /// writes zeros and counts one misuse. - /// @param minLengthOut Receives the shortest length, meters; may be null. - /// @param maxLengthOut Receives the longest length, meters; may be null. + /// @param minLengthOut Receives the shortest length, meters; 0 on + /// failure. May be null. + /// @param maxLengthOut Receives the longest length, meters; 0 on + /// failure. May be null. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. @@ -887,7 +899,7 @@ extern "C" /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API float m2Joint_GetMaxMotor(const m2World* world, m2JointId jointId); + M2_API float m2Joint_GetMaxMotorEffort(const m2World* world, m2JointId jointId); /// Tells whether a motor drives. /// @@ -938,9 +950,9 @@ extern "C" /// /// @param world The world. /// @param jointId A revolute, prismatic or wheel joint; another type - /// or a stale id writes zeros. - /// @param lowerOut Receives the lower limit; may be null. - /// @param upperOut Receives the upper limit; may be null. + /// or a stale id writes zeros and counts one misuse. + /// @param lowerOut Receives the lower limit; 0 on failure. May be null. + /// @param upperOut Receives the upper limit; 0 on failure. May be null. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. @@ -950,9 +962,12 @@ extern "C" /// Reads the break limits m2Joint_SetBreakLimits sets. /// /// @param world The world. - /// @param jointId The joint; a stale id writes zeros. - /// @param maxForceOut Receives the force threshold; may be null. - /// @param maxTorqueOut Receives the torque threshold; may be null. + /// @param jointId The joint; a stale id writes zeros and counts + /// one misuse. + /// @param maxForceOut Receives the force threshold; 0 on failure. May + /// be null. + /// @param maxTorqueOut Receives the torque threshold; 0 on failure. May + /// be null. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. @@ -1047,9 +1062,9 @@ extern "C" M2_API float m2Joint_GetAngularSeparation(const m2World* world, m2JointId jointId); /// Moves a motor joint's target, as a moving platform does every frame. - /// The torque budget is set with m2Joint_SetMaxMotor. Another joint - /// type, an angular offset beyond M2_ANGLE_LIMIT or a linear offset beyond - /// M2_LOCAL_LIMIT counts one misuse. + /// The budgets are set with m2MotorJoint_SetMaxForce and + /// m2MotorJoint_SetMaxTorque. Another joint type, an angular offset beyond M2_ANGLE_LIMIT or a + /// linear offset beyond M2_LOCAL_LIMIT counts one misuse. /// /// @param world The world. /// @param jointId A motor joint. @@ -1060,6 +1075,29 @@ extern "C" M2_API void m2MotorJoint_SetOffsets(m2World* world, m2JointId jointId, m2Vec2 linearOffset, float angularOffset); + /// Sets a motor joint's force budget, the most force its point rows + /// apply to hold B at its offset. Another joint type, or a budget + /// outside 0 to M2_FORCE_LIMIT, counts one misuse. + /// + /// @param world The world. + /// @param jointId A motor joint. + /// @param maxForce The budget, N. + /// @par Thread safety + /// Safe from any thread; a world is used by one thread at a time. + M2_API void m2MotorJoint_SetMaxForce(m2World* world, m2JointId jointId, float maxForce); + + /// Sets a motor joint's torque budget, the most torque its angle row + /// applies to hold B at its angular offset; the same budget + /// m2Joint_SetMaxMotorEffort sets. Another joint type, or a budget + /// outside 0 to M2_FORCE_LIMIT, counts one misuse. + /// + /// @param world The world. + /// @param jointId A motor joint. + /// @param maxTorque The budget, N m. + /// @par Thread safety + /// Safe from any thread; a world is used by one thread at a time. + M2_API void m2MotorJoint_SetMaxTorque(m2World* world, m2JointId jointId, float maxTorque); + /// Reads a motor joint's linear offset. /// /// @param world The world. @@ -1090,6 +1128,16 @@ extern "C" /// several readers at once. M2_API float m2MotorJoint_GetMaxForce(const m2World* world, m2JointId jointId); + /// Reads a motor joint's torque budget. + /// + /// @param world The world. + /// @param jointId A motor joint. + /// @return The torque, N m; 0 for another joint type or a stale id. + /// @par Thread safety + /// Safe from any thread; a world is used by one thread at a time, or by + /// several readers at once. + M2_API float m2MotorJoint_GetMaxTorque(const m2World* world, m2JointId jointId); + /// Reads a motor joint's correction factor. /// /// @param world The world. @@ -1191,7 +1239,7 @@ extern "C" /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API float m2RatchetJoint_GetRatchet(const m2World* world, m2JointId jointId); + M2_API float m2RatchetJoint_GetToothAngle(const m2World* world, m2JointId jointId); /// Reads a ratchet's tooth phase. /// @@ -1236,28 +1284,28 @@ extern "C" /// Lists the world's joints in ascending slot order. /// /// @param world The world. - /// @param ids Receives up to capacity joints; may be null to - /// measure. - /// @param capacity The length of ids. + /// @param idsOut Receives up to capacity joints; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of idsOut, not below 0. /// @return The number of joints, which may exceed capacity. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API int32_t m2World_GetJoints(const m2World* world, m2JointId* ids, int32_t capacity); + M2_API int32_t m2World_GetJoints(const m2World* world, m2JointId* idsOut, int32_t capacity); /// Lists the joints attached to a body, in ascending slot order. /// /// @param world The world. /// @param bodyId The body; a stale id counts one misuse. - /// @param ids Receives up to capacity joints; may be null to - /// measure. - /// @param capacity The length of ids. + /// @param idsOut Receives up to capacity joints; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of idsOut, not below 0. /// @return The number of joints, which may exceed capacity; 0 for a /// stale id. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API int32_t m2Body_GetJoints(const m2World* world, m2BodyId bodyId, m2JointId* ids, + M2_API int32_t m2Body_GetJoints(const m2World* world, m2BodyId bodyId, m2JointId* idsOut, int32_t capacity); static const m2JointId m2_nullJointId = {0, 0}; diff --git a/third_party/maul2d/include/maul2d/particle.h b/third_party/maul2d/include/maul2d/particle.h index f0d052637..f9ca5d679 100644 --- a/third_party/maul2d/include/maul2d/particle.h +++ b/third_party/maul2d/include/maul2d/particle.h @@ -42,7 +42,11 @@ extern "C" /// droplets bead up and cling instead of spreading. Viscous particles /// drag their neighbors (honey, syrup). Powder grains repel when packed /// tighter than the rest stride and never cohere (sand, rubble, dust). - typedef enum m2ParticleFlags + /// The type has a fixed width, unlike a C enum's, so a def holding it + /// has one layout in every compiler, C and C++. + typedef uint32_t m2ParticleFlags; + + enum { m2_waterParticle = 0, m2_tensileParticle = 1u << 0, @@ -50,7 +54,7 @@ extern "C" m2_powderParticle = 1u << 2, m2_springParticle = 1u << 3, // pairwise springs captured by a fill m2_elasticParticle = 1u << 4, // shape-restoring triads captured by a fill - } m2ParticleFlags; + }; /// The def of a particle, or of every particle of a polygon fill. /// Initialize it with m2DefaultParticleDef. @@ -58,7 +62,7 @@ extern "C" { m2Pos2 position; // world space; for a fill, where the polygon's frame sits m2Vec2 velocity; // m/s, within M2_SPEED_LIMIT per axis - uint32_t flags; // m2ParticleFlags values, ored together + m2ParticleFlags flags; // m2_*Particle values, ored together; no other bit int32_t internalValue; // the cookie m2DefaultParticleDef sets } m2ParticleDef; @@ -71,15 +75,17 @@ extern "C" M2_API m2ParticleDef m2DefaultParticleDef(void); /// Creates one particle. A full pool is also counted in - /// m2Counters.particlePoolFull; pace emitters off + /// m2Counters.particleCreatesRefused; pace emitters off /// m2World_GetParticleCount. Journaled. /// /// @param world The world. /// @param def The particle, from m2DefaultParticleDef. /// @param particleOut Receives the particle; the null id on failure. - /// @return m2_success; m2_errorInvalid when def or particleOut is - /// null, def was not initialized by m2DefaultParticleDef or a - /// value of it is NaN or not finite where it has no bound; + /// @return m2_success; m2_errorInvalid when the world, def or + /// particleOut is null, def was not initialized by + /// m2DefaultParticleDef, a value of it is NaN or not finite + /// where it has no bound, or its flags hold a bit no + /// m2_*Particle flag has; /// m2_errorLimit for a velocity past M2_SPEED_LIMIT; /// m2_errorCapacity when the pool is full, a pool of 0 included, /// which counts no misuse; m2_errorBusy from host code the world @@ -95,7 +101,8 @@ extern "C" /// /// @param world The world. /// @param particleId The particle. - /// @return m2_success; m2_errorStale when it is already gone, which + /// @return m2_success; m2_errorInvalid for a null world; + /// m2_errorStale when it is already gone, which /// counts no misuse; m2_errorBusy from host code the world is /// running. /// @par Thread safety @@ -127,11 +134,11 @@ extern "C" /// /// @param world The world. /// @param particleId The particle. - /// @return The m2ParticleFlags it was created with; 0 for a stale id. + /// @return The m2_*Particle flags it was created with; 0 for a stale id. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API uint32_t m2Particle_GetFlags(const m2World* world, m2ParticleId particleId); + M2_API m2ParticleFlags m2Particle_GetFlags(const m2World* world, m2ParticleId particleId); /// Gives a particle a lifetime. It counts down by each step's length, /// and the particle is destroyed at the end of the step it reaches @@ -222,13 +229,15 @@ extern "C" /// and not a sliver. Its normals are recomputed from /// its vertices. /// @param countOut Receives the number of particles created; 0 when - /// the fill is refused. + /// the fill is refused, except that m2_errorBusy leaves + /// it as it was. /// @return m2_success; m2_errorCapacity when the pool ran out before /// the polygon was full, a pool of 0 included, which counts no - /// misuse and counts once in m2Counters.particlePoolFull; + /// misuse and counts once in m2Counters.particleCreatesRefused; /// m2_errorInvalid when an argument is null, def was not - /// initialized by m2DefaultParticleDef, a value of it is NaN or - /// the polygon is not one a shape would take; + /// initialized by m2DefaultParticleDef, a value of it is NaN, + /// its flags hold a bit no m2_*Particle flag has, or the + /// polygon is not one a shape would take; /// m2_errorLimit for a velocity or a polygon past its published /// bound, or a polygon wider than M2_MAX_FILL_COLUMNS strides; /// m2_errorBusy from host code the world is running. @@ -245,28 +254,29 @@ extern "C" /// /// @param world The world. /// @param box The box, in world space. - /// @param ids Receives up to capacity particles; may be null to - /// measure. - /// @param capacity The length of ids. + /// @param idsOut Receives up to capacity particles; zeros over the whole capacity on + /// failure. May be null with a capacity of 0 to measure. + /// @param capacity The length of idsOut, not below 0. /// @return The number of particles inside, which may exceed capacity; - /// 0 for a non-finite corner, which counts one misuse. + /// 0 for a non-finite or inverted box, which counts one misuse. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. M2_API int32_t m2World_OverlapParticlesAabb(const m2World* world, m2WorldAabb box, - m2ParticleId* ids, int32_t capacity); + m2ParticleId* idsOut, int32_t capacity); /// Lists the live particles in ascending slot order. /// /// @param world The world. - /// @param ids Receives up to capacity particles; may be null to - /// measure. - /// @param capacity The length of ids. + /// @param idsOut Receives up to capacity particles; zeros over the whole capacity on + /// failure. May be null with a capacity of 0 to measure. + /// @param capacity The length of idsOut, not below 0. /// @return The number of particles, which may exceed capacity. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API int32_t m2World_GetParticles(const m2World* world, m2ParticleId* ids, int32_t capacity); + M2_API int32_t m2World_GetParticles(const m2World* world, m2ParticleId* idsOut, + int32_t capacity); #ifdef __cplusplus } diff --git a/third_party/maul2d/include/maul2d/shape.h b/third_party/maul2d/include/maul2d/shape.h index cc1ab4248..780d81ab1 100644 --- a/third_party/maul2d/include/maul2d/shape.h +++ b/third_party/maul2d/include/maul2d/shape.h @@ -24,7 +24,7 @@ extern "C" #define M2_MAX_POLYGON_VERTICES 8 // The most points m2DecomposeOutline takes, and the most pieces -// m2World_ShatterBody takes in one call. +// m2Body_Shatter takes in one call. #define M2_MAX_OUTLINE_POINTS 64 #define M2_MAX_SHATTER_PIECES 64 @@ -67,7 +67,9 @@ extern "C" float radius; } m2Polygon; - /// A two-sided line segment. + /// A two-sided line segment. It has no area, so it never collides with + /// another segment or a chain segment; a sensor among them still + /// detects the other. typedef struct m2Segment { m2Vec2 point1; @@ -77,7 +79,8 @@ extern "C" /// One link of a chain: the segment and the ghost points of its /// neighbors, which keep bodies from snagging on the seams. Built by /// m2CreateChain. It collides on one side only: solid on the right when - /// walking from point1 to point2. + /// walking from point1 to point2. Like a segment, it never collides + /// with a segment or another chain segment. typedef struct m2ChainSegment { m2Segment segment; @@ -85,14 +88,19 @@ extern "C" m2Vec2 ghost2; } m2ChainSegment; - typedef enum m2ShapeType + /// A shape's geometry kind: one of the m2_*Shape constants. The type + /// has a fixed width, unlike a C enum's, so it crosses the interface the + /// same in every compiler, C and C++. + typedef int32_t m2ShapeType; + + enum { m2_circleShape = 0, m2_capsuleShape = 1, m2_polygonShape = 2, m2_segmentShape = 3, m2_chainSegmentShape = 4, - } m2ShapeType; + }; /// The def of a shape. Initialize it with m2DefaultShapeDef. typedef struct m2ShapeDef @@ -168,8 +176,9 @@ extern "C" /// shape pool ran out before the last segment (chainOut names /// the shorter chain) or the chain or shape pool has no room at /// all (chainOut is the null id); m2_errorStale for a stale - /// body; m2_errorInvalid for a bad def or a null chainOut; - /// m2_errorLimit for a point or value past its published bound; + /// body; m2_errorInvalid for a null world, a bad def or a null + /// chainOut; m2_errorLimit for a point or value past its + /// published bound; /// m2_errorBusy from host code the world is running. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. @@ -181,7 +190,8 @@ extern "C" /// /// @param world The world. /// @param chainId The chain. - /// @return m2_success; m2_errorStale when the chain is already gone; + /// @return m2_success; m2_errorInvalid for a null world; + /// m2_errorStale when the chain is already gone; /// m2_errorBusy from host code the world is running. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. @@ -247,7 +257,9 @@ extern "C" /// Builds the convex hull of a loose point cloud: near points are /// welded, collinear ones merged, the rest wrapped in a deterministic - /// order. The way from a sprite's points to a collision shape. + /// order. The way from a sprite's points to a collision shape. A + /// merged point lies within 0.01 m of the edge that replaces it, so a + /// curve that needs more vertices to stay that close is m2_errorLimit. /// /// @param points The cloud. /// @param count The number of points, 3 to M2_MAX_HULL_POINTS. @@ -269,25 +281,28 @@ extern "C" /// dropped by validation; a clean outline loses nothing, and one that /// would lose every piece is refused. /// - /// @param points The outline, up to M2_MAX_OUTLINE_POINTS points. - /// @param count The number of points. - /// @param pieces Receives up to capacity pieces; may be null to - /// measure. - /// @param capacity The length of pieces. - /// @param countOut Receives the number of pieces, which may exceed - /// capacity; left as it was on failure. + /// @param points The outline, up to M2_MAX_OUTLINE_POINTS points. + /// @param count The number of points. + /// @param piecesOut Receives up to capacity pieces; may be null with a + /// capacity of 0 to measure. Left as it was on + /// failure: a piece is written only once it passes + /// validation, and the call fails only when none does. + /// @param capacity The length of piecesOut, not below 0. + /// @param countOut Receives the number of pieces, which may exceed + /// capacity; left as it was on failure. /// @return m2_success; m2_errorLimit for more than /// M2_MAX_OUTLINE_POINTS points or a point past M2_LOCAL_LIMIT; /// m2_errorInvalid for too few points, clockwise winding, an /// outline too thin for any piece to pass validation, an /// outline that is not simple (an edge crossing another, a /// vertex within 0.005 m of an edge it does not end, a repeated - /// vertex, an edge folding back over its neighbor) or a null - /// argument. It takes no world, so a refusal counts nothing. + /// vertex, an edge folding back over its neighbor), a negative + /// capacity, a null piecesOut with a positive capacity or another + /// null argument. It takes no world, so a refusal counts nothing. /// @par Thread safety /// Safe from any thread. M2_NODISCARD M2_API m2Result m2DecomposeOutline(const m2Vec2* points, int32_t count, - m2Polygon* pieces, int32_t capacity, + m2Polygon* piecesOut, int32_t capacity, int32_t* countOut); /// Breaks a dynamic body into one new dynamic body per piece, all at @@ -308,20 +323,22 @@ extern "C" /// capacity 0. /// @param capacity The length of bodiesOut, not below 0. /// @param countOut Receives the number of bodies made, which may - /// exceed capacity; 0 on failure. + /// exceed capacity; 0 on failure, except that + /// m2_errorBusy leaves it as it was. /// @return m2_success; m2_errorStale for a stale body; m2_errorInvalid /// for a body that is not dynamic, an invalid piece or piece - /// count, or a bad output argument; m2_errorLimit for more than - /// M2_MAX_SHATTER_PIECES pieces or a piece past its bounds, as + /// count, a null world or a bad output argument; m2_errorLimit + /// for more than M2_MAX_SHATTER_PIECES pieces or a piece past its + /// bounds, as /// m2MakePolygon gives them; m2_errorCapacity when the /// body or shape pool cannot seat every piece, which counts no /// misuse; m2_errorBusy from host code the world is running. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. - M2_NODISCARD M2_API m2Result m2World_ShatterBody(m2World* world, m2BodyId bodyId, - const m2Polygon* pieces, int32_t pieceCount, - m2BodyId* bodiesOut, int32_t capacity, - int32_t* countOut); + M2_NODISCARD M2_API m2Result m2Body_Shatter(m2World* world, m2BodyId bodyId, + const m2Polygon* pieces, int32_t pieceCount, + m2BodyId* bodiesOut, int32_t capacity, + int32_t* countOut); /// Builds a box centered on the body origin. /// @@ -414,7 +431,8 @@ extern "C" /// /// @param world The world. /// @param shapeId The shape. - /// @return m2_success; m2_errorStale when the shape is already gone; + /// @return m2_success; m2_errorInvalid for a null world; + /// m2_errorStale when the shape is already gone; /// m2_errorBusy from host code the world is running. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. @@ -522,9 +540,10 @@ extern "C" /// @param world The world. /// @param shapeId The shape; a stale id counts one misuse and /// writes zeros. - /// @param categoryBitsOut Receives the categories; may be null. - /// @param maskBitsOut Receives the mask; may be null. - /// @param groupIndexOut Receives the group; may be null. + /// @param categoryBitsOut Receives the categories; 0 on failure. May be + /// null. + /// @param maskBitsOut Receives the mask; 0 on failure. May be null. + /// @param groupIndexOut Receives the group; 0 on failure. May be null. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. @@ -542,9 +561,9 @@ extern "C" /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API m2Circle m2Shape_GetCircle(const m2World* world, m2ShapeId shapeId); + M2_API m2Circle m2CircleShape_GetCircle(const m2World* world, m2ShapeId shapeId); - /// Reads a capsule shape's geometry, as m2Shape_GetCircle does. + /// Reads a capsule shape's geometry, as m2CircleShape_GetCircle does. /// /// @param world The world. /// @param shapeId The shape; a stale id or a shape of another type @@ -553,9 +572,9 @@ extern "C" /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API m2Capsule m2Shape_GetCapsule(const m2World* world, m2ShapeId shapeId); + M2_API m2Capsule m2CapsuleShape_GetCapsule(const m2World* world, m2ShapeId shapeId); - /// Reads a polygon shape's geometry, as m2Shape_GetCircle does. + /// Reads a polygon shape's geometry, as m2CircleShape_GetCircle does. /// /// @param world The world. /// @param shapeId The shape; a stale id or a shape of another type @@ -564,9 +583,9 @@ extern "C" /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API m2Polygon m2Shape_GetPolygon(const m2World* world, m2ShapeId shapeId); + M2_API m2Polygon m2PolygonShape_GetPolygon(const m2World* world, m2ShapeId shapeId); - /// Reads a segment shape's geometry, as m2Shape_GetCircle does. + /// Reads a segment shape's geometry, as m2CircleShape_GetCircle does. /// /// @param world The world. /// @param shapeId The shape; a stale id or a shape of another type @@ -575,9 +594,9 @@ extern "C" /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API m2Segment m2Shape_GetSegment(const m2World* world, m2ShapeId shapeId); + M2_API m2Segment m2SegmentShape_GetSegment(const m2World* world, m2ShapeId shapeId); - /// Reads a chain link's geometry and ghosts, as m2Shape_GetCircle does. + /// Reads a chain link's geometry and ghosts, as m2CircleShape_GetCircle does. /// /// @param world The world. /// @param shapeId The link; a stale id or a shape of another type @@ -586,7 +605,8 @@ extern "C" /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API m2ChainSegment m2Shape_GetChainSegment(const m2World* world, m2ShapeId shapeId); + M2_API m2ChainSegment m2ChainSegmentShape_GetChainSegment(const m2World* world, + m2ShapeId shapeId); /// Reads a shape's density. /// @@ -645,42 +665,42 @@ extern "C" /// /// @param world The world. /// @param bodyId The body; a stale id counts one misuse. - /// @param ids Receives up to capacity shapes; may be null to - /// measure. - /// @param capacity The length of ids. + /// @param idsOut Receives up to capacity shapes; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of idsOut, not below 0. /// @return The number of shapes, which may exceed capacity; 0 for a /// stale id. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API int32_t m2Body_GetShapes(const m2World* world, m2BodyId bodyId, m2ShapeId* ids, + M2_API int32_t m2Body_GetShapes(const m2World* world, m2BodyId bodyId, m2ShapeId* idsOut, int32_t capacity); /// Lists the world's chains in ascending slot order. /// /// @param world The world. - /// @param ids Receives up to capacity chains; may be null to - /// measure. - /// @param capacity The length of ids. + /// @param idsOut Receives up to capacity chains; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of idsOut, not below 0. /// @return The number of chains, which may exceed capacity. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API int32_t m2World_GetChains(const m2World* world, m2ChainId* ids, int32_t capacity); + M2_API int32_t m2World_GetChains(const m2World* world, m2ChainId* idsOut, int32_t capacity); /// Lists a chain's links in ascending slot order. /// /// @param world The world. /// @param chainId The chain; a stale id counts one misuse. - /// @param ids Receives up to capacity shapes; may be null to - /// measure. - /// @param capacity The length of ids. + /// @param idsOut Receives up to capacity shapes; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of idsOut, not below 0. /// @return The number of links, which may exceed capacity; 0 for a /// stale id. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API int32_t m2Chain_GetShapes(const m2World* world, m2ChainId chainId, m2ShapeId* ids, + M2_API int32_t m2Chain_GetShapes(const m2World* world, m2ChainId chainId, m2ShapeId* idsOut, int32_t capacity); /// Sets the friction of every link of a chain, as one journaled @@ -704,6 +724,31 @@ extern "C" /// Safe from any thread; a world is used by one thread at a time. M2_API void m2Chain_SetRestitution(m2World* world, m2ChainId chainId, float restitution); + /// Reads a chain's friction: the chain's own material, as its def or + /// m2Chain_SetFriction last set it, not any link's. m2Shape_SetFriction + /// on one link changes that link alone and leaves this value as it is. + /// + /// @param world The world. + /// @param chainId The chain; a stale id counts one misuse. + /// @return The friction; 0 for a stale id. + /// @par Thread safety + /// Safe from any thread; a world is used by one thread at a time, or by + /// several readers at once. + M2_API float m2Chain_GetFriction(const m2World* world, m2ChainId chainId); + + /// Reads a chain's restitution: the chain's own material, as its def or + /// m2Chain_SetRestitution last set it, not any link's. + /// m2Shape_SetRestitution on one link changes that link alone and leaves + /// this value as it is. + /// + /// @param world The world. + /// @param chainId The chain; a stale id counts one misuse. + /// @return The restitution; 0 for a stale id. + /// @par Thread safety + /// Safe from any thread; a world is used by one thread at a time, or by + /// several readers at once. + M2_API float m2Chain_GetRestitution(const m2World* world, m2ChainId chainId); + /// Reads the body a shape is attached to. /// /// @param world The world. @@ -828,8 +873,8 @@ extern "C" /// @param translation The ray, origin + t * translation for t in /// [0, 1]. /// @param filter Which shapes the ray sees. - /// @param hitOut Receives the closest hit; left as it was on a - /// miss. + /// @param hitOut Receives the closest hit; zeros, with the null + /// shape id, on a miss or a refusal. /// @return True on a hit; false on a miss, and for a non-finite argument /// or a null hitOut, which count one misuse. /// @par Thread safety @@ -845,7 +890,8 @@ extern "C" /// @param shapeId The shape; a stale id counts one misuse. /// @param origin The start, in world space. /// @param translation The ray. - /// @param hitOut Receives the hit; left as it was on a miss. + /// @param hitOut Receives the hit; zeros, with the null shape id, + /// on a miss or a refusal. /// @return True on a hit; false on a miss, and for a stale id, a /// non-finite argument or a null hitOut, which count one misuse. /// @par Thread safety @@ -861,17 +907,17 @@ extern "C" /// @param world The world. /// @param origin The start, in world space. /// @param translation The ray. - /// @param hits Receives up to capacity hits; may be null to - /// measure. - /// @param capacity The length of hits. /// @param filter Which shapes the ray sees. + /// @param hitsOut Receives up to capacity hits; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of hitsOut, not below 0. /// @return The number of hits, which may exceed capacity; 0 for a /// non-finite argument, which counts one misuse. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. M2_API int32_t m2World_CastRayAll(const m2World* world, m2Pos2 origin, m2Vec2 translation, - m2CastHit* hits, int32_t capacity, m2QueryFilter filter); + m2QueryFilter filter, m2CastHit* hitsOut, int32_t capacity); /// Sweeps a circle and lists every hit, as m2World_CastRayAll does. /// @@ -879,18 +925,19 @@ extern "C" /// @param circle The circle, in its own frame. /// @param origin The circle's pose at the start. /// @param translation The sweep. - /// @param hits Receives up to capacity hits; may be null to - /// measure. - /// @param capacity The length of hits. /// @param filter Which shapes the sweep sees. + /// @param hitsOut Receives up to capacity hits; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of hitsOut, not below 0. /// @return The number of hits, which may exceed capacity; 0 for an /// invalid argument, which counts one misuse. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. M2_API int32_t m2World_CastCircleAll(const m2World* world, const m2Circle* circle, - m2Transform origin, m2Vec2 translation, m2CastHit* hits, - int32_t capacity, m2QueryFilter filter); + m2Transform origin, m2Vec2 translation, + m2QueryFilter filter, m2CastHit* hitsOut, + int32_t capacity); /// Sweeps a capsule and lists every hit, as m2World_CastRayAll does. /// @@ -898,18 +945,19 @@ extern "C" /// @param capsule The capsule, in its own frame. /// @param origin The capsule's pose at the start. /// @param translation The sweep. - /// @param hits Receives up to capacity hits; may be null to - /// measure. - /// @param capacity The length of hits. /// @param filter Which shapes the sweep sees. + /// @param hitsOut Receives up to capacity hits; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of hitsOut, not below 0. /// @return The number of hits, which may exceed capacity; 0 for an /// invalid argument, which counts one misuse. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. M2_API int32_t m2World_CastCapsuleAll(const m2World* world, const m2Capsule* capsule, - m2Transform origin, m2Vec2 translation, m2CastHit* hits, - int32_t capacity, m2QueryFilter filter); + m2Transform origin, m2Vec2 translation, + m2QueryFilter filter, m2CastHit* hitsOut, + int32_t capacity); /// Sweeps a polygon and lists every hit, as m2World_CastRayAll does. /// @@ -917,18 +965,19 @@ extern "C" /// @param polygon The polygon, in its own frame. /// @param origin The polygon's pose at the start. /// @param translation The sweep. - /// @param hits Receives up to capacity hits; may be null to - /// measure. - /// @param capacity The length of hits. /// @param filter Which shapes the sweep sees. + /// @param hitsOut Receives up to capacity hits; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of hitsOut, not below 0. /// @return The number of hits, which may exceed capacity; 0 for an /// invalid argument, which counts one misuse. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. M2_API int32_t m2World_CastPolygonAll(const m2World* world, const m2Polygon* polygon, - m2Transform origin, m2Vec2 translation, m2CastHit* hits, - int32_t capacity, m2QueryFilter filter); + m2Transform origin, m2Vec2 translation, + m2QueryFilter filter, m2CastHit* hitsOut, + int32_t capacity); /// A plane touching a character mover, from m2World_CollideMover. typedef struct m2MoverPlane @@ -940,7 +989,7 @@ extern "C" } m2MoverPlane; /// Planes past this count take no part in m2SolveMover. -#define M2_MOVER_PLANES 16 +#define M2_MAX_MOVER_PLANES 16 /// The result of m2SolveMover. typedef struct m2MoverMove @@ -950,32 +999,33 @@ extern "C" } m2MoverMove; /// Gathers the planes touching a posed capsule, one per nearby shape, - /// in ascending shape order. A collar of four linear slops lets a + /// in ascending shape order; an array too short for them all holds the + /// planes of the lowest shape slots. A collar of four linear slops lets a /// controller see a wall before it reaches it. A chain link counts only /// from its solid side. Use m2World_CastCapsuleClosest for the sweep. /// - /// @param world The world. - /// @param mover The capsule, in its own frame. - /// @param origin The capsule's pose. - /// @param planes Receives up to capacity planes; may be null to - /// measure. - /// @param capacity The length of planes. - /// @param filter Which shapes the mover sees. + /// @param world The world. + /// @param mover The capsule, in its own frame. + /// @param origin The capsule's pose. + /// @param filter Which shapes the mover sees. + /// @param planesOut Receives up to capacity planes; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of planesOut, not below 0. /// @return The number of planes, which may exceed capacity; 0 for an /// invalid argument, which counts one misuse. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. M2_API int32_t m2World_CollideMover(const m2World* world, const m2Capsule* mover, - m2Transform origin, m2MoverPlane* planes, int32_t capacity, - m2QueryFilter filter); + m2Transform origin, m2QueryFilter filter, + m2MoverPlane* planesOut, int32_t capacity); /// Turns a wished translation into the closest one no plane blocks, and /// reports which planes it rests on. /// /// @param wish The translation the character wants. /// @param planes The planes from m2World_CollideMover. - /// @param count The number of planes; only the first M2_MOVER_PLANES + /// @param count The number of planes; only the first M2_MAX_MOVER_PLANES /// take part. /// @return The translation to apply and the planes that stopped part of /// the wish. @@ -1006,8 +1056,8 @@ extern "C" /// @param origin The circle's pose at the start. /// @param translation The sweep. /// @param filter Which shapes the sweep sees. - /// @param hitOut Receives the closest hit; left as it was on a - /// miss. + /// @param hitOut Receives the closest hit; zeros, with the null + /// shape id, on a miss or a refusal. /// @return True on a hit; false on a miss, and for an invalid argument or /// a null hitOut, which count one misuse. /// @par Thread safety @@ -1025,8 +1075,8 @@ extern "C" /// @param origin The capsule's pose at the start. /// @param translation The sweep. /// @param filter Which shapes the sweep sees. - /// @param hitOut Receives the closest hit; left as it was on a - /// miss. + /// @param hitOut Receives the closest hit; zeros, with the null + /// shape id, on a miss or a refusal. /// @return True on a hit; false on a miss, and for an invalid argument or /// a null hitOut, which count one misuse. /// @par Thread safety @@ -1044,8 +1094,8 @@ extern "C" /// @param origin The polygon's pose at the start. /// @param translation The sweep. /// @param filter Which shapes the sweep sees. - /// @param hitOut Receives the closest hit; left as it was on a - /// miss. + /// @param hitOut Receives the closest hit; zeros, with the null + /// shape id, on a miss or a refusal. /// @return True on a hit; false on a miss, and for an invalid argument or /// a null hitOut, which count one misuse. /// @par Thread safety @@ -1062,18 +1112,18 @@ extern "C" /// @param world The world. /// @param circle The circle, in its own frame. /// @param origin The circle's pose. - /// @param ids Receives up to capacity shapes; may be null to - /// measure. - /// @param capacity The length of ids. /// @param filter Which shapes the query sees. + /// @param idsOut Receives up to capacity shapes; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of idsOut, not below 0. /// @return The number of shapes, which may exceed capacity; 0 for an /// invalid argument, which counts one misuse. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. M2_API int32_t m2World_OverlapCircle(const m2World* world, const m2Circle* circle, - m2Transform origin, m2ShapeId* ids, int32_t capacity, - m2QueryFilter filter); + m2Transform origin, m2QueryFilter filter, + m2ShapeId* idsOut, int32_t capacity); /// Lists the live shapes touching a posed capsule, as /// m2World_OverlapCircle does. @@ -1081,18 +1131,18 @@ extern "C" /// @param world The world. /// @param capsule The capsule, in its own frame. /// @param origin The capsule's pose. - /// @param ids Receives up to capacity shapes; may be null to - /// measure. - /// @param capacity The length of ids. /// @param filter Which shapes the query sees. + /// @param idsOut Receives up to capacity shapes; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of idsOut, not below 0. /// @return The number of shapes, which may exceed capacity; 0 for an /// invalid argument, which counts one misuse. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. M2_API int32_t m2World_OverlapCapsule(const m2World* world, const m2Capsule* capsule, - m2Transform origin, m2ShapeId* ids, int32_t capacity, - m2QueryFilter filter); + m2Transform origin, m2QueryFilter filter, + m2ShapeId* idsOut, int32_t capacity); /// Lists the live shapes touching a posed polygon, as /// m2World_OverlapCircle does. @@ -1100,35 +1150,35 @@ extern "C" /// @param world The world. /// @param polygon The polygon, in its own frame. /// @param origin The polygon's pose. - /// @param ids Receives up to capacity shapes; may be null to - /// measure. - /// @param capacity The length of ids. /// @param filter Which shapes the query sees. + /// @param idsOut Receives up to capacity shapes; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of idsOut, not below 0. /// @return The number of shapes, which may exceed capacity; 0 for an /// invalid argument, which counts one misuse. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. M2_API int32_t m2World_OverlapPolygon(const m2World* world, const m2Polygon* polygon, - m2Transform origin, m2ShapeId* ids, int32_t capacity, - m2QueryFilter filter); + m2Transform origin, m2QueryFilter filter, + m2ShapeId* idsOut, int32_t capacity); /// Lists the live shapes whose tight box overlaps a world box, in /// ascending slot order. /// /// @param world The world. /// @param box The box, in world space. - /// @param results Receives up to capacity shapes; may be null to - /// measure. - /// @param capacity The length of results. /// @param filter Which shapes the query sees. + /// @param idsOut Receives up to capacity shapes; zeros over the whole capacity on failure. + /// May be null with a capacity of 0 to measure. + /// @param capacity The length of idsOut, not below 0. /// @return The number of shapes, which may exceed capacity; 0 for a /// non-finite or inverted box, which counts one misuse. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API int32_t m2World_OverlapAabb(const m2World* world, m2WorldAabb box, m2ShapeId* results, - int32_t capacity, m2QueryFilter filter); + M2_API int32_t m2World_OverlapAabb(const m2World* world, m2WorldAabb box, m2QueryFilter filter, + m2ShapeId* idsOut, int32_t capacity); #ifdef __cplusplus } diff --git a/third_party/maul2d/include/maul2d/world.h b/third_party/maul2d/include/maul2d/world.h index 27f8b69e1..cbeaf73b2 100644 --- a/third_party/maul2d/include/maul2d/world.h +++ b/third_party/maul2d/include/maul2d/world.h @@ -28,13 +28,13 @@ extern "C" typedef struct m2World m2World; /// A range job: run items [startIndex, endIndex). - typedef void m2TaskFn(int32_t startIndex, int32_t endIndex, void* taskContext); + typedef void m2TaskFunction(int32_t startIndex, int32_t endIndex, void* taskContext); /// Splits [0, itemCount) into subranges of at least minRange and runs /// task over them on the host's scheduler; the return value is handed /// to finishTask, which waits until all of them are done. - typedef void* m2EnqueueTaskFn(m2TaskFn* task, int32_t itemCount, int32_t minRange, - void* taskContext, void* userContext); - typedef void m2FinishTaskFn(void* userTask, void* userContext); + typedef void* m2EnqueueTaskFunction(m2TaskFunction* task, int32_t itemCount, int32_t minRange, + void* taskContext, void* userContext); + typedef void m2FinishTaskFunction(void* userTask, void* userContext); /// The def of a world. Initialize it with m2DefaultWorldDef. typedef struct m2WorldDef @@ -53,9 +53,10 @@ extern "C" /// particleCapacity above 0 builds the particle system into the /// world for its whole life, so every snapshot has the same shape /// and rollback stays safe at any point in history. Its parameters - /// are fixed here like every other physics setting. + /// are fixed here like every other physics setting, and checked + /// even when particleCapacity is 0. int32_t particleCapacity; // 0: no fluids - int32_t fluidVolumeCapacity; // 0: no buoyancy volumes + int32_t waterVolumeCapacity; // 0: no buoyancy volumes float particleRadius; // meters, M2_MIN_PARTICLE_RADIUS to M2_LOCAL_LIMIT float particleDensity; // kg/m^2; a particle weighs M2_MIN_MASS or more float particleGravityScale; // within M2_GRAVITY_SCALE_LIMIT @@ -79,8 +80,8 @@ extern "C" /// when every subrange is done. Both null, the default, runs the /// work serially on the calling thread. Any executor and any split /// give the same bits. - m2EnqueueTaskFn* enqueueTask; - m2FinishTaskFn* finishTask; + m2EnqueueTaskFunction* enqueueTask; + m2FinishTaskFunction* finishTask; void* userTaskContext; /// The allocator the world takes all its memory from, kept for its /// lifetime. Zeroed, it is the C library's. Set both functions or @@ -123,7 +124,7 @@ extern "C" /// therefore makes n * 2^32 objects over a world's life, snapshots /// included; then its creates return m2_errorCapacity. /// - /// MISUSE. m2Counters.misuse counts the bugs of the calling code: an + /// MISUSE. m2Counters.misuseCount counts the bugs of the calling code: an /// argument a correct program never passes, a stale id, a writer called /// from host code the world is running. Untrusted input is not misuse: /// a snapshot or journal tape that fails its checks, and a value one of @@ -146,7 +147,7 @@ extern "C" /// Returns a world def with the library's defaults, a valid cookie and /// the ABI version of the headers the program is built with. It is /// built in the program, so m2CreateWorld can refuse a def made for - /// another release's binary interface. + /// another minor version's binary interface. /// /// @return The default def. /// @par Thread safety @@ -184,7 +185,12 @@ extern "C" M2_NODISCARD M2_API m2Result m2CreateWorld(const m2WorldDef* def, m2World** worldOut); /// Destroys a world and everything in it, and returns its memory to its - /// allocator. Ids into it become meaningless. + /// allocator. Ids into it become meaningless. The world empties before + /// its first free: the allocator's free hook may read it back and finds + /// every id stale and every list empty, and every writer it calls is + /// refused as busy. This call is a writer too: host code the world runs + /// (a task hook, a debug draw callback, the allocator) that calls it + /// counts one misuse, and the world stays. /// /// @param world The world; null does nothing. /// @par Thread safety @@ -229,7 +235,7 @@ extern "C" /// -linearDrag * shapeArea * (bodyVelocity - windVelocity), added like /// gravity, so larger bodies catch more wind and everything drifts /// toward the wind's speed. Unlike gravity it wakes no sleeper, since - /// wind is meant to gust. Regional wind is a fluid volume of density 0. + /// wind is meant to gust. Regional wind is a water volume of density 0. /// A velocity beyond M2_SPEED_LIMIT on either axis or a drag outside 0 to /// M2_RATE_LIMIT counts one misuse. Journaled. /// @@ -244,8 +250,10 @@ extern "C" /// Reads the global wind. /// /// @param world The world; null writes zeros. - /// @param velocityOut Receives the air's velocity; may be null. - /// @param linearDragOut Receives the drag coefficient; may be null. + /// @param velocityOut Receives the air's velocity; (0, 0) for a null + /// world. May be null. + /// @param linearDragOut Receives the drag coefficient; 0 for a null + /// world. May be null. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. @@ -305,7 +313,8 @@ extern "C" /// host renders. Polygon vertices arrive body-local with the body's /// f64 origin and rotation, so a renderer can subtract its camera in /// doubles and stay steady far from the origin. A null callback skips - /// that primitive. Colors are 0xRRGGBB hints. + /// that primitive. Colors are 0xRRGGBB hints. Start from a zeroed + /// struct, so a field a later minor release adds stays off. typedef struct m2DebugDraw { void (*drawPolygon)(const m2Vec2* localVertices, int32_t count, m2Pos2 origin, @@ -336,38 +345,51 @@ extern "C" /// several readers at once. M2_API void m2World_Draw(const m2World* world, const m2DebugDraw* draw); - /// Wall-clock times of the last step. They are not deterministic and - /// never enter a snapshot or a hash. + /// Wall-clock times of the last step's phases, each in milliseconds. + /// They are not deterministic and never enter a snapshot or a hash. typedef struct m2Profile { - float stepMs; // the whole m2World_Step - float pairsMs; // broadphase and pair update - float contactsMs; // narrowphase manifolds - float solveMs; // solver, continuous collision included - float sleepMs; // islands and sleep accounting + float step; // the whole m2World_Step + float broadphase; // broadphase and pair update + float narrowphase; // narrowphase manifolds + float solve; // solver, continuous collision included + float sleep; // islands and sleep accounting } m2Profile; - /// Counts derived from world state, so they are deterministic. + /// What a world holds and what it did: observer data. Reading the + /// counters never touches the simulation, a snapshot or the hash, and + /// twins given the same calls report the same counts. The counts of + /// live objects, awake bodies, pairs and steps come from world state, + /// so a restore brings them back with it. The last-step counts + /// (constraintCount, colorCount, overflowCount and the three counts of + /// drops) describe the last step this world completed and are zero + /// before it; particleCreatesRefused and misuseCount add up over the + /// world's life. Neither kind is state: it is not hashed or + /// snapshotted, and a restore leaves it as it is, so right after a + /// rollback the last-step counts still describe the step before it. A + /// refused m2World_ReplayJournal leaves every count as it was before + /// the call; only misuse by host code the replay ran stays counted. typedef struct m2Counters { - int32_t bodies; - int32_t awakeBodies; - int32_t shapes; - int32_t joints; - int32_t pairs; - int32_t touchingPairs; - int32_t constraints; // solved last step - int32_t graphColors; // colors used last step - int32_t overflowConstraints; // in the serial bucket last step + int32_t bodyCount; + int32_t awakeCount; // enabled, awake dynamic bodies + int32_t shapeCount; + int32_t jointCount; + int32_t contactCount; // live broadphase pairs + int32_t touchingCount; // pairs whose shapes touch + int32_t constraintCount; // solved last step + int32_t colorCount; // solver graph colors used last step + int32_t overflowCount; // constraints in the serial bucket last step M2_ALIGN8 uint64_t stepCount; // Counted in every build. Poll them once per frame and alarm on // growth: there is no per-call callback, so healthy calls pay - // nothing for the diagnosis. - int32_t pairOverflow; // body pairs dropped last step (pair table full) - int32_t particlePairOverflow; // particle pairs dropped last step (budget) - int32_t particleBodyOverflow; // particle body contacts dropped last step - M2_ALIGN8 uint64_t particlePoolFull; // creates refused by a full pool, cumulative - M2_ALIGN8 uint64_t misuse; // calls refused as misuse, cumulative + // nothing for the diagnosis. The three counts of drops stop at + // INT32_MAX. + int32_t pairsDropped; // body pairs dropped last step (pair table full) + int32_t particlePairsDropped; // particle pairs dropped last step (budget) + int32_t particleBodyContactsDropped; // particle body contacts dropped last step + M2_ALIGN8 uint64_t particleCreatesRefused; // creates refused by a full pool, cumulative + M2_ALIGN8 uint64_t misuseCount; // calls refused as misuse, cumulative } m2Counters; /// Sums the kinetic energy of the awake dynamic bodies, in f64 and in @@ -401,18 +423,22 @@ extern "C" /// Advances the simulation. Identical worlds given identical calls /// reach identical bits on every supported platform. Feed the same dt - /// every step: 1/60 with 4 substeps is the tuned default. A dt outside - /// M2_MIN_STEP to M2_STEP_LIMIT, or a substep count below 1 or - /// above M2_MAX_SUBSTEPS, changes nothing and counts one misuse. - /// Journaled. + /// every step: 1/60 with 4 substeps is the tuned default. A refused + /// step changes nothing, and every refusal but a null world's counts + /// one misuse. Journaled. /// /// @param world The world. /// @param dt The step, seconds, from M2_MIN_STEP (1e-6) /// to M2_STEP_LIMIT (10). /// @param substepCount The solver substeps, 1 to M2_MAX_SUBSTEPS. + /// @return m2_success; m2_errorInvalid for a null world, a dt that + /// is NaN or 0 or below, or a substep count below 1; + /// m2_errorLimit for a dt outside M2_MIN_STEP to + /// M2_STEP_LIMIT or a substep count above M2_MAX_SUBSTEPS; + /// m2_errorBusy from host code the world is running. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. - M2_API void m2World_Step(m2World* world, float dt, int32_t substepCount); + M2_NODISCARD M2_API m2Result m2World_Step(m2World* world, float dt, int32_t substepCount); /// Counts the steps taken since creation; a restore brings back the /// snapshot's count. @@ -432,31 +458,39 @@ extern "C" /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API int64_t m2World_SnapshotSize(const m2World* world); + M2_API int64_t m2World_GetSnapshotSize(const m2World* world); - /// Writes the whole simulation state into caller memory. + /// Writes the whole simulation state into caller memory. A failed + /// snapshot zeroes the buffer, so it never holds what looks like part + /// of a snapshot: a host that keeps a ring of snapshot buffers sizes + /// them with m2World_GetSnapshotSize first, or snapshots into a spare + /// buffer and swaps it in on success. /// - /// @param world The world. - /// @param buffer Receives the snapshot. - /// @param capacity The length of buffer, in bytes. - /// @param bytesOut Receives the bytes written; with m2_errorCapacity, - /// the size needed; 0 on any other failure. - /// @return m2_success; m2_errorInvalid for a null argument; + /// @param world The world. + /// @param bufferOut Receives the snapshot; zeros over the whole + /// capacity on failure. + /// @param capacity The length of bufferOut, in bytes, not below 0. + /// @param bytesOut Receives the bytes written; with m2_errorCapacity, + /// the size needed; 0 on any other failure. + /// @return m2_success; m2_errorInvalid for a null argument or a negative + /// capacity; /// m2_errorCapacity when capacity is below - /// m2World_SnapshotSize, with nothing written. + /// m2World_GetSnapshotSize. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_NODISCARD M2_API m2Result m2World_Snapshot(const m2World* world, void* buffer, + M2_NODISCARD M2_API m2Result m2World_Snapshot(const m2World* world, void* bufferOut, int64_t capacity, int64_t* bytesOut); /// Restores a snapshot of a world with the same shape: the same /// capacities and the same particle radius in its def. Everything, /// bodies, id pools and the step count included, returns to the - /// snapshot's instant, bit for bit, and the event streams clear. Only - /// the release that wrote a snapshot reads it (M2_ABI_VERSION). The - /// buffer is checked before any byte lands: a header from another - /// release, build or world shape, an index or count out of range, + /// snapshot's instant, bit for bit, and the event streams clear. End + /// events a destroy or a disable queued for the next step are state: + /// the snapshot carries them, and the next step delivers them. Only + /// the minor version that wrote a snapshot reads it (M2_ABI_VERSION). + /// The buffer is checked before any byte lands: a header from another + /// minor version, build or world shape, an index or count out of range, /// flags that are not flags and non-finite state are refused, and the /// world stays as it was. The checks make every index safe; they do not /// rebuild every structural invariant (tree shape, list links), so @@ -466,10 +500,10 @@ extern "C" /// @param world The world. /// @param buffer The snapshot. /// @param size Its length, in bytes. - /// @return m2_success; m2_errorVersion for a snapshot another release - /// or build wrote, and m2_errorFormat for other bytes that fail - /// the checks, a world of another shape included, neither of - /// which counts misuse; m2_errorInvalid for a null buffer, which + /// @return m2_success; m2_errorVersion for a snapshot another minor + /// version or build wrote, and m2_errorFormat for other bytes + /// that fail the checks, a world of another shape included, + /// neither of which counts misuse; m2_errorInvalid for a null buffer, which /// counts one; m2_errorBusy from host code the world is running. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. @@ -477,7 +511,7 @@ extern "C" /// Hashes every piece of state the step reads and a call can change: /// the world's gravity, wind, sleeping switch and step count; every - /// live body, shape, chain, joint, particle and fluid volume, each + /// live body, shape, chain, joint, particle and water volume, each /// field keyed by its slot (pose, velocities, forces, mass data, /// damping, gravity scale, locks, dominance, sleep and enable state; /// geometry, materials, filter and sensor flag; joint anchors, @@ -526,7 +560,7 @@ extern "C" M2_ALIGN8 uint64_t contacts; // pairs, their touching flags and manifolds M2_ALIGN8 uint64_t joints; // joint settings and impulses, and the joint id pool M2_ALIGN8 uint64_t particles; // particles and the jelly nets, and the particle id pool - M2_ALIGN8 uint64_t volumes; // fluid volumes and their id pool + M2_ALIGN8 uint64_t volumes; // water volumes and their id pool } m2WorldHashParts; /// Hashes each part of the world. Compare two runs part by part each @@ -541,14 +575,14 @@ extern "C" /// several readers at once. M2_API m2WorldHashParts m2World_GetHashParts(const m2World* world); - /// A fluid volume of a world: a 1-based slot and a generation. - typedef struct m2FluidVolumeId + /// A water volume of a world: a 1-based slot and a generation. + typedef struct m2WaterVolumeId { uint32_t index1; // 1-based slot, 0 = null uint32_t generation; - } m2FluidVolumeId; + } m2WaterVolumeId; - static const m2FluidVolumeId m2_nullFluidVolumeId = {0, 0}; + static const m2WaterVolumeId m2_nullWaterVolumeId = {0, 0}; /// Water without particles: an axis-aligned region and a horizontal /// surface. Every awake dynamic body whose origin lies in the region @@ -556,51 +590,53 @@ extern "C" /// for circles and polygons, a box fraction for capsules and /// segments), linear and angular drag, and the flow. Far cheaper than a /// particle pool where one would be overkill. - typedef struct m2FluidVolumeDef + typedef struct m2WaterVolumeDef { - m2Pos2 regionLower; // the region, world space - m2Pos2 regionUpper; + m2WorldAabb bounds; // the region, world space; upper above lower on both axes M2_ALIGN8 double surface; // the waterline, world y float density; // buoyant force per submerged area; 0 to M2_DENSITY_LIMIT float linearDrag; // velocity damping per submerged area; 0 to M2_RATE_LIMIT float angularDrag; // spin damping; 0 to M2_RATE_LIMIT m2Vec2 flow; // the current, m/s, within M2_SPEED_LIMIT per axis M2_ALIGN8 uint64_t userData; - int32_t internalValue; // the cookie m2DefaultFluidVolumeDef sets - } m2FluidVolumeDef; + int32_t internalValue; // the cookie m2DefaultWaterVolumeDef sets + } m2WaterVolumeDef; - /// Returns a fluid volume def: density 2, linear drag 1, angular drag - /// 0.5, no flow, an empty region at the origin. + /// Returns a water volume def: density 2, linear drag 1, angular drag + /// 0.5, no flow, and a box of one point at the origin, which + /// m2CreateWaterVolume refuses until the bounds are set. /// - /// @return The default def; set the region and the surface. + /// @return The default def; set the bounds and the surface. /// @par Thread safety /// Safe from any thread. - M2_API m2FluidVolumeDef m2DefaultFluidVolumeDef(void); + M2_API m2WaterVolumeDef m2DefaultWaterVolumeDef(void); - /// Creates a fluid volume. Journaled. + /// Creates a water volume. Journaled. /// /// @param world The world. - /// @param def The volume, from m2DefaultFluidVolumeDef. + /// @param def The volume, from m2DefaultWaterVolumeDef. /// @param volumeOut Receives the volume; the null id on failure. - /// @return m2_success; m2_errorInvalid for a bad def or a null - /// argument; m2_errorLimit for a value past its published - /// bound; m2_errorCapacity when the volume pool is full, a pool - /// of 0 included, which counts no misuse; m2_errorBusy from - /// host code the world is running. + /// @return m2_success; m2_errorInvalid for a bad def, a box with no + /// area among them (upper at or below lower on either axis, + /// which can never hold a body), or a null argument; + /// m2_errorLimit for a value past its published bound; m2_errorCapacity when the volume + /// pool is full, a pool of 0 included, which counts no misuse; m2_errorBusy from host + /// code the world is running. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. - M2_NODISCARD M2_API m2Result m2CreateFluidVolume(m2World* world, const m2FluidVolumeDef* def, - m2FluidVolumeId* volumeOut); + M2_NODISCARD M2_API m2Result m2CreateWaterVolume(m2World* world, const m2WaterVolumeDef* def, + m2WaterVolumeId* volumeOut); - /// Destroys a fluid volume. Journaled. + /// Destroys a water volume. Journaled. /// /// @param world The world. /// @param volumeId The volume. - /// @return m2_success; m2_errorStale when it is already gone; + /// @return m2_success; m2_errorInvalid for a null world; + /// m2_errorStale when it is already gone; /// m2_errorBusy from host code the world is running. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. - M2_NODISCARD M2_API m2Result m2DestroyFluidVolume(m2World* world, m2FluidVolumeId volumeId); + M2_NODISCARD M2_API m2Result m2DestroyWaterVolume(m2World* world, m2WaterVolumeId volumeId); /// Tells whether a volume id names a live volume. Counts no misuse. /// @@ -610,7 +646,7 @@ extern "C" /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API bool m2FluidVolume_IsValid(const m2World* world, m2FluidVolumeId volumeId); + M2_API bool m2WaterVolume_IsValid(const m2World* world, m2WaterVolumeId volumeId); /// Moves a volume's waterline: a rising tide, a draining tank. A stale /// id or a non-finite surface counts one misuse. Journaled. @@ -620,7 +656,7 @@ extern "C" /// @param surface The new waterline, world y. /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time. - M2_API void m2FluidVolume_SetSurface(m2World* world, m2FluidVolumeId volumeId, double surface); + M2_API void m2WaterVolume_SetSurface(m2World* world, m2WaterVolumeId volumeId, double surface); /// Reads a volume's waterline. /// @@ -630,7 +666,7 @@ extern "C" /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API double m2FluidVolume_GetSurface(const m2World* world, m2FluidVolumeId volumeId); + M2_API double m2WaterVolume_GetSurface(const m2World* world, m2WaterVolumeId volumeId); /// Reads a volume's game data. /// @@ -640,7 +676,18 @@ extern "C" /// @par Thread safety /// Safe from any thread; a world is used by one thread at a time, or by /// several readers at once. - M2_API uint64_t m2FluidVolume_GetUserData(const m2World* world, m2FluidVolumeId volumeId); + M2_API uint64_t m2WaterVolume_GetUserData(const m2World* world, m2WaterVolumeId volumeId); + + /// Sets a volume's opaque game data, kept through snapshots and journals. + /// + /// @param world The world. + /// @param volumeId The volume; a stale id changes nothing and counts one + /// misuse. + /// @param userData Any value. + /// @par Thread safety + /// Safe from any thread; a world is used by one thread at a time. + M2_API void m2WaterVolume_SetUserData(m2World* world, m2WaterVolumeId volumeId, + uint64_t userData); /// Returns a journal tape's fixed cost: its header and the snapshot it /// embeds. Size a tape as this plus room for the operations. @@ -659,9 +706,9 @@ extern "C" /// /// @param world The world. /// @param buffer Receives the tape. - /// @param capacity The length of buffer, in bytes. - /// @return m2_success; m2_errorInvalid for a null buffer or a capacity - /// a size_t cannot hold; m2_errorCapacity when capacity is below + /// @param capacity The length of buffer, in bytes, not below 0. + /// @return m2_success; m2_errorInvalid for a null buffer, a negative + /// capacity or one a size_t cannot hold; m2_errorCapacity when capacity is below /// m2World_GetJournalBaseSize; m2_errorState when the world is /// already recording; m2_errorBusy from host code the world is /// running. @@ -674,8 +721,9 @@ extern "C" /// it is never cut short silently. /// /// @param world The world. - /// @param bytesOut Receives the tape's size; 0 on failure. - /// @return m2_success; m2_errorInvalid for a null bytesOut; + /// @param bytesOut Receives the tape's size; 0 on failure, except that + /// m2_errorBusy leaves it as it was. + /// @return m2_success; m2_errorInvalid for a null world or bytesOut; /// m2_errorCapacity when the operations overflowed the buffer; /// m2_errorState when the world was not recording; m2_errorBusy /// from host code the world is running. @@ -685,22 +733,24 @@ extern "C" /// Replays a tape: restores the snapshot it embeds and applies its /// operations through the same calls that recorded them. It is atomic: - /// a tape that is malformed or truncated, or that recreates an object - /// under another id than the recording saw, is refused and the world - /// stays as it was. Only the release that wrote a tape replays it - /// (M2_ABI_VERSION); a tape from another release, build or world shape - /// is refused too. + /// a tape that is malformed or truncated, that carries a value one of + /// its calls refuses (a void call included), or that recreates an + /// object under another id than the recording saw, is refused and the + /// world stays as it was, its event streams and counters included; only + /// misuse by host code the replay ran stays counted. Only the minor + /// version that wrote a tape replays it (M2_ABI_VERSION); a tape from + /// another minor version, build or world shape is refused too. /// /// @param world The world. /// @param data The tape. /// @param size Its length, in bytes. /// @return m2_success; m2_errorVersion for a tape, or its embedded - /// snapshot, another release or build wrote, and m2_errorFormat - /// for another tape that fails its checks (a record the world - /// refuses included), neither of which counts misuse; - /// m2_errorInvalid for a null data pointer or a size a size_t - /// cannot hold, which counts one; m2_errorState while the world - /// is recording; m2_errorBusy from host code the world is + /// snapshot, another minor version or build wrote, and + /// m2_errorFormat for another tape that fails its checks (a + /// record the world refuses included), neither of which counts + /// misuse; m2_errorInvalid for a null data pointer or a size a + /// size_t cannot hold, which counts one; m2_errorState while the + /// world is recording; m2_errorBusy from host code the world is /// running; m2_errorCapacity when the allocator cannot give the /// replay its scratch memory. /// @par Thread safety diff --git a/third_party/maul2d/src/body.c b/third_party/maul2d/src/body.c index 8a691d0ae..d15ad061f 100644 --- a/third_party/maul2d/src/body.c +++ b/third_party/maul2d/src/body.c @@ -36,7 +36,7 @@ m2BodyDef m2DefaultBodyDef(void) def.rotation = (m2Rot){1.0f, 0.0f}; def.gravityScale = 1.0f; def.enableSleep = true; - def.isEnabled = true; + def.enable = true; def.internalValue = M2_BODY_COOKIE; return def; } @@ -55,12 +55,13 @@ static m2Result BodyDefCheck(const m2BodyDef* def) result = m2BothChecks(result, m2CheckSpeed(def->angularVelocity)); result = m2BothChecks(result, m2CheckGravityScale(def->gravityScale)); result = m2BothChecks(result, m2CheckRate(def->linearDamping)); - return m2BothChecks(result, m2CheckRate(def->angularDamping)); + result = m2BothChecks(result, m2CheckRate(def->angularDamping)); + return m2BothChecks(result, m2CheckSleepThreshold(def->sleepThreshold)); } // A static body never moves, so its def carries no velocity, as its // velocity setters take none. -static bool StaticDefIsStill(const m2BodyDef* def) +M2_NODISCARD static bool StaticDefIsStill(const m2BodyDef* def) { return def->type != m2_staticBody || (def->linearVelocity.x == 0.0f && def->linearVelocity.y == 0.0f && @@ -86,11 +87,12 @@ static void RecordBodyDef(m2BodyDef* out, const m2BodyDef* def) out->motionLocks.linearY = def->motionLocks.linearY; out->motionLocks.angularZ = def->motionLocks.angularZ; out->enableSleep = def->enableSleep; - out->isEnabled = def->isEnabled; + out->enable = def->enable; out->dominance = def->dominance; - out->isBullet = def->isBullet; + out->enableBullet = def->enableBullet; out->userData = def->userData; out->internalValue = def->internalValue; + out->sleepThreshold = def->sleepThreshold; } static M2_NOINLINE m2Result CreateBodyWorker(m2World* world, const m2BodyDef* def, @@ -140,9 +142,10 @@ static M2_NOINLINE m2Result CreateBodyWorker(m2World* world, const m2BodyDef* de (uint8_t)((def->motionLocks.linearX ? M2_LOCK_LINEAR_X : 0u) | (def->motionLocks.linearY ? M2_LOCK_LINEAR_Y : 0u)); world->bodies.sleepEnables[index] = def->enableSleep ? 1 : 0; + world->bodies.sleepThresholds[index] = m2ResolveSleepThreshold(def->sleepThreshold); world->bodies.forces[index] = (m2Vec2){0.0f, 0.0f}; world->bodies.torques[index] = 0.0f; - world->bodies.disabled[index] = def->isEnabled ? 0 : 1; + world->bodies.disabled[index] = def->enable ? 0 : 1; world->bodies.dominances[index] = def->dominance; world->bodies.userData[index] = def->userData; world->bodies.types[index] = (uint8_t)def->type; @@ -155,7 +158,7 @@ static M2_NOINLINE m2Result CreateBodyWorker(m2World* world, const m2BodyDef* de world->bodies.asleep[index] = 0; world->bodies.sleepTimes[index] = 0.0f; world->bodies.sleepStreak[index] = 0; - world->bodies.bullets[index] = def->isBullet ? 1 : 0; + world->bodies.bullets[index] = def->enableBullet ? 1 : 0; if (index + 1 > world->bodies.maxBodyIndex) { world->bodies.maxBodyIndex = index + 1; @@ -300,7 +303,7 @@ M2_BODY_GETTER(uint64_t, m2Body_GetUserData, world->bodies.userData[index], 0) // The value contract of each body parameter channel. Live setters and // replay both pass through it, so a tape can never write what the API // would refuse. -static bool BodyParamValid(uint8_t param, float value) +M2_NODISCARD static bool BodyParamValid(uint8_t param, float value) { switch (param) { @@ -314,6 +317,8 @@ static bool BodyParamValid(uint8_t param, float value) case m2_bodyParamLockLinearX: case m2_bodyParamLockLinearY: return value == 0.0f || value == 1.0f; + case m2_bodyParamSleepThreshold: + return m2ValidSleepThreshold(value); default: return false; } @@ -350,6 +355,9 @@ bool m2SetBodyParamInternal(m2World* world, m2BodyId bodyId, uint8_t param, floa case m2_bodyParamGravityScale: world->bodies.gravityScales[index] = value; break; + case m2_bodyParamSleepThreshold: + world->bodies.sleepThresholds[index] = m2ResolveSleepThreshold(value); + break; case m2_bodyParamFixedRotation: // Fixed rotation is a mass property: inertia recomputes, spin // stops now (a frozen axis with leftover spin is a lie). @@ -399,7 +407,7 @@ static M2_NOINLINE void BodySetLinearDampingWorker(m2World* world, m2BodyId body { return; } - m2SetBodyParamInternal(world, bodyId, m2_bodyParamLinearDamping, damping); + (void)m2SetBodyParamInternal(world, bodyId, m2_bodyParamLinearDamping, damping); } void m2Body_SetLinearDamping(m2World* world, m2BodyId bodyId, float damping) @@ -417,7 +425,7 @@ static M2_NOINLINE void BodySetAngularDampingWorker(m2World* world, m2BodyId bod { return; } - m2SetBodyParamInternal(world, bodyId, m2_bodyParamAngularDamping, damping); + (void)m2SetBodyParamInternal(world, bodyId, m2_bodyParamAngularDamping, damping); } void m2Body_SetAngularDamping(m2World* world, m2BodyId bodyId, float damping) @@ -435,7 +443,7 @@ static M2_NOINLINE void BodySetGravityScaleWorker(m2World* world, m2BodyId bodyI { return; } - m2SetBodyParamInternal(world, bodyId, m2_bodyParamGravityScale, scale); + (void)m2SetBodyParamInternal(world, bodyId, m2_bodyParamGravityScale, scale); } void m2Body_SetGravityScale(m2World* world, m2BodyId bodyId, float scale) @@ -457,9 +465,10 @@ static M2_NOINLINE void BodySetMotionLocksWorker(m2World* world, m2BodyId bodyId // A stale id refuses once, on the first channel. if (m2SetBodyParamInternal(world, bodyId, m2_bodyParamLockLinearX, locks.linearX ? 1.0f : 0.0f)) { - m2SetBodyParamInternal(world, bodyId, m2_bodyParamLockLinearY, locks.linearY ? 1.0f : 0.0f); - m2SetBodyParamInternal(world, bodyId, m2_bodyParamFixedRotation, - locks.angularZ ? 1.0f : 0.0f); + (void)m2SetBodyParamInternal(world, bodyId, m2_bodyParamLockLinearY, + locks.linearY ? 1.0f : 0.0f); + (void)m2SetBodyParamInternal(world, bodyId, m2_bodyParamFixedRotation, + locks.angularZ ? 1.0f : 0.0f); } } @@ -501,7 +510,7 @@ static M2_NOINLINE void BodyEnableSleepWorker(m2World* world, m2BodyId bodyId, b { return; } - m2SetBodyParamInternal(world, bodyId, m2_bodyParamEnableSleep, flag ? 1.0f : 0.0f); + (void)m2SetBodyParamInternal(world, bodyId, m2_bodyParamEnableSleep, flag ? 1.0f : 0.0f); } void m2Body_EnableSleep(m2World* world, m2BodyId bodyId, bool flag) @@ -513,6 +522,25 @@ void m2Body_EnableSleep(m2World* world, m2BodyId bodyId, bool flag) M2_BODY_GETTER(bool, m2Body_IsSleepEnabled, world->bodies.sleepEnables[index] != 0, false) +static M2_NOINLINE void BodySetSleepThresholdWorker(m2World* world, m2BodyId bodyId, + float threshold) +{ + if (m2CheckWriter(world) != m2_success) + { + return; + } + (void)m2SetBodyParamInternal(world, bodyId, m2_bodyParamSleepThreshold, threshold); +} + +void m2Body_SetSleepThreshold(m2World* world, m2BodyId bodyId, float threshold) +{ + uint64_t env = m2FloatEnvEnter(); + BodySetSleepThresholdWorker(world, bodyId, threshold); + m2FloatEnvLeave(env); +} + +M2_BODY_GETTER(float, m2Body_GetSleepThreshold, world->bodies.sleepThresholds[index], 0.0f) + static M2_NOINLINE void BodySetTypeWorker(m2World* world, m2BodyId bodyId, m2BodyType type) { if (m2CheckWriter(world) != m2_success) @@ -678,7 +706,7 @@ static M2_NOINLINE void BodySetBulletWorker(m2World* world, m2BodyId bodyId, boo world->bodies.bullets[index] = next; } -void m2Body_SetBullet(m2World* world, m2BodyId bodyId, bool flag) +void m2Body_EnableBullet(m2World* world, m2BodyId bodyId, bool flag) { uint64_t env = m2FloatEnvEnter(); BodySetBulletWorker(world, bodyId, flag); @@ -732,6 +760,7 @@ static void EnableBody(m2World* world, m2BodyId bodyId, int32_t index) world->broadphase.proxyIds[s] = m2TreeInsert(&world->broadphase.trees[tree], world->broadphase.treeNodes[tree], m2Fatten(m2ShapeTightAabb(world, s)), s); + M2_ASSERT(world->broadphase.proxyIds[s] != M2_NULL_NODE); // the pool never fills m2PushMoved(world, s); } world->bodies.asleep[index] = 0; @@ -764,7 +793,7 @@ static M2_NOINLINE void BodySetEnabledWorker(m2World* world, m2BodyId bodyId, bo } } -void m2Body_SetEnabled(m2World* world, m2BodyId bodyId, bool flag) +void m2Body_Enable(m2World* world, m2BodyId bodyId, bool flag) { uint64_t env = m2FloatEnvEnter(); BodySetEnabledWorker(world, bodyId, flag); @@ -813,7 +842,7 @@ void m2Body_SetDominance(m2World* world, m2BodyId bodyId, int8_t dominance) // inverse does, as the inverse of a mass of FLT_MAX does. The inertia is // taken about the center as given, so no shift from another point cancels // a small one away; 0 gives a body that cannot turn. -static bool MassDataInverses(m2MassData massData, float* invMass, float* invInertia) +M2_NODISCARD static bool MassDataInverses(m2MassData massData, float* invMass, float* invInertia) { if (!m2ValidMass(massData.mass) || !m2LocalVec2(massData.center) || !m2ValidInertia(massData.centralInertia)) diff --git a/third_party/maul2d/src/body.h b/third_party/maul2d/src/body.h index b831f8415..cad7f34db 100644 --- a/third_party/maul2d/src/body.h +++ b/third_party/maul2d/src/body.h @@ -12,7 +12,8 @@ #define M2_BODY_COOKIE (M2_COOKIE ^ ((int32_t)sizeof(m2BodyDef) << 8) ^ 2) // The journaled parameter channel (codes in journal.h). -bool m2SetBodyParamInternal(m2World* world, m2BodyId bodyId, uint8_t param, float value); +M2_NODISCARD bool m2SetBodyParamInternal(m2World* world, m2BodyId bodyId, uint8_t param, + float value); // Holds a body's velocity within M2_SPEED_LIMIT after an impulse, a blast // or a shatter, as the solver does after each of its pushes. diff --git a/third_party/maul2d/src/body_motion.c b/third_party/maul2d/src/body_motion.c index 8a2d80f44..afebfa84c 100644 --- a/third_party/maul2d/src/body_motion.c +++ b/third_party/maul2d/src/body_motion.c @@ -388,23 +388,31 @@ void m2Body_SetTransform(m2World* world, m2BodyId bodyId, m2Pos2 position, m2Rot m2FloatEnvLeave(env); } -static M2_NOINLINE void BodySetTargetTransformWorker(m2World* world, m2BodyId bodyId, - m2Pos2 position, m2Rot rotation, float dt) +static M2_NOINLINE m2Result BodySetTargetTransformWorker(m2World* world, m2BodyId bodyId, + m2Pos2 position, m2Rot rotation, float dt) { - if (m2CheckWriter(world) != m2_success) + m2Result writer = m2CheckWriter(world); + if (writer != m2_success) { - return; + return writer; } - int32_t index = world != NULL ? m2BodySlot(world, bodyId) : -1; - if (index < 0 || world->bodies.types[index] == (uint8_t)m2_staticBody || !m2ValidTimeStep(dt) || - !m2FinitePos2(position) || !m2UnitRot(rotation)) + int32_t index = m2BodySlot(world, bodyId); + if (index < 0) { - m2Refuse(world, m2_errorInvalid); - return; + return m2Refuse(world, m2_errorStale); + } + m2Result checked = m2BothChecks( + m2CheckTimeStep(dt), m2CheckValid(world->bodies.types[index] != (uint8_t)m2_staticBody && + m2FinitePos2(position) && m2UnitRot(rotation))); + if (checked != m2_success) + { + return m2Refuse(world, checked); } // Velocities that land the pose in one step; applied through the // journaled setters, so replays get this for free. A pose too far to - // reach within M2_SPEED_LIMIT is refused before either is set. + // reach within M2_SPEED_LIMIT is refused before either is set, and + // counts no misuse: the distance runs from where the step left the + // body, the world's state, not from an argument alone. float invDt = 1.0f / dt; m2Transform xf = world->bodies.transforms[index]; m2Vec2 v = {m2WorldOffset(position.x - xf.p.x) * invDt, @@ -412,17 +420,18 @@ static M2_NOINLINE void BodySetTargetTransformWorker(m2World* world, m2BodyId bo float w = m2UnwindAngleWork(m2RelativeJointAngle(xf.q, rotation)) * invDt; if (!m2ValidVelocity(v) || !m2ValidSpeed(w)) { - m2Refuse(world, m2_errorInvalid); - return; + return m2_errorLimit; } m2Body_SetLinearVelocity(world, bodyId, v); m2Body_SetAngularVelocity(world, bodyId, w); + return m2_success; } -void m2Body_SetTargetTransform(m2World* world, m2BodyId bodyId, m2Pos2 position, m2Rot rotation, - float dt) +m2Result m2Body_SetTargetTransform(m2World* world, m2BodyId bodyId, m2Pos2 position, m2Rot rotation, + float dt) { uint64_t env = m2FloatEnvEnter(); - BodySetTargetTransformWorker(world, bodyId, position, rotation, dt); + m2Result result = BodySetTargetTransformWorker(world, bodyId, position, rotation, dt); m2FloatEnvLeave(env); + return result; } diff --git a/third_party/maul2d/src/body_read.c b/third_party/maul2d/src/body_read.c index 6bc2486eb..64d935a01 100644 --- a/third_party/maul2d/src/body_read.c +++ b/third_party/maul2d/src/body_read.c @@ -161,23 +161,22 @@ m2Vec2 m2Body_GetWorldPointVelocity(const m2World* world, m2BodyId bodyId, m2Pos return result; } -int32_t m2Body_GetJoints(const m2World* world, m2BodyId bodyId, m2JointId* ids, int32_t capacity) +int32_t m2Body_GetJoints(const m2World* world, m2BodyId bodyId, m2JointId* idsOut, int32_t capacity) { int32_t index = world != NULL ? m2BodySlot(world, bodyId) : -1; - if (index < 0) + if (index < 0 || !m2ListOutputValid(idsOut, capacity)) { - m2Refuse(world, m2_errorInvalid); - return 0; + return m2RefuseList(world, idsOut, capacity, sizeof(*idsOut)); } int32_t total = 0; for (int32_t e = world->joints.bodyJointHead[index]; e != -1; e = world->joints.jointEdgeNext[e]) { int32_t j = e >> 1; - if (ids != NULL && total < capacity) + if (idsOut != NULL && total < capacity) { m2JointId id = {(uint32_t)(j + 1), world->joints.jointGenerations[j]}; - ids[total] = id; + idsOut[total] = id; } total += 1; } @@ -187,6 +186,13 @@ int32_t m2Body_GetJoints(const m2World* world, m2BodyId bodyId, m2JointId* ids, static M2_NOINLINE m2Vec2 BodyGetLocalPointVelocityWorker(const m2World* world, m2BodyId bodyId, m2Vec2 localPoint) { + // The id is checked once here: the two calls below then see a live + // body and count nothing, so a stale id counts one misuse, not two. + if (world == NULL || m2BodySlot(world, bodyId) < 0) + { + m2Refuse(world, m2_errorInvalid); + return (m2Vec2){0.0f, 0.0f}; + } return m2Body_GetWorldPointVelocity(world, bodyId, m2Body_GetWorldPoint(world, bodyId, localPoint)); } @@ -359,7 +365,7 @@ m2Vec2 m2Body_GetLocalCenter(const m2World* world, m2BodyId bodyId) return result; } -bool m2Body_IsBullet(const m2World* world, m2BodyId bodyId) +bool m2Body_IsBulletEnabled(const m2World* world, m2BodyId bodyId) { int32_t index = world != NULL ? m2BodySlot(world, bodyId) : -1; if (index < 0) @@ -389,13 +395,12 @@ float m2Body_GetGravityScale(const m2World* world, m2BodyId bodyId) return result; } -int32_t m2Body_GetShapes(const m2World* world, m2BodyId bodyId, m2ShapeId* ids, int32_t capacity) +int32_t m2Body_GetShapes(const m2World* world, m2BodyId bodyId, m2ShapeId* idsOut, int32_t capacity) { int32_t bodyIndex = world != NULL ? m2BodySlot(world, bodyId) : -1; - if (bodyIndex < 0) + if (bodyIndex < 0 || !m2ListOutputValid(idsOut, capacity)) { - m2Refuse(world, m2_errorInvalid); - return 0; + return m2RefuseList(world, idsOut, capacity, sizeof(*idsOut)); } int32_t total = 0; for (int32_t i = 0; i < world->shapes.maxShapeIndex; ++i) @@ -404,9 +409,9 @@ int32_t m2Body_GetShapes(const m2World* world, m2BodyId bodyId, m2ShapeId* ids, { continue; } - if (ids != NULL && total < capacity) + if (idsOut != NULL && total < capacity) { - ids[total] = m2MakeShapeId(world, i); + idsOut[total] = m2MakeShapeId(world, i); } total += 1; } diff --git a/third_party/maul2d/src/broadphase.c b/third_party/maul2d/src/broadphase.c index 510c1d45a..41b7aba92 100644 --- a/third_party/maul2d/src/broadphase.c +++ b/third_party/maul2d/src/broadphase.c @@ -54,16 +54,53 @@ static int CompareI32(const void* a, const void* b) return ia < ib ? -1 : (ia > ib ? 1 : 0); } -static bool ShapeIsDynamic(const m2World* world, int32_t shapeIndex) +m2PairSide m2ShapePairSide(const m2World* world, int32_t shapeIndex) { - return world->bodies.types[world->shapes.shapeBody[shapeIndex]] == (uint8_t)m2_dynamicBody; + m2PairSide side = {world->shapes.shapeCategory[shapeIndex], + world->shapes.shapeMask[shapeIndex], + world->shapes.shapeGroup[shapeIndex], + world->shapes.shapeGeometry[shapeIndex].type, + world->shapes.shapeSensor[shapeIndex] != 0, + world->bodies.types[world->shapes.shapeBody[shapeIndex]]}; + return side; +} + +// Two shapes on static bodies never pair, a sensor among them or not: a +// static mover queries only the dynamic tree. Two non-dynamic shapes pair +// only through a sensor, and two sensors never detect each other. Two +// shapes without area, a segment or a chain segment against another, never +// hold each other: two lines have no inside to push each other out of, so a +// body that meets one faster than the contact margin per step passes +// through it. A sensor among them still pairs. Then the filters: one group +// decides alone, a negative one keeping the two apart and a positive one +// pairing them; otherwise each shape's category must be in the other's +// mask. +bool m2SidesMayPair(const m2PairSide* a, const m2PairSide* b) +{ + if ((a->sensor && b->sensor) || + (a->bodyType == (uint8_t)m2_staticBody && b->bodyType == (uint8_t)m2_staticBody)) + { + return false; + } + bool dynamic = a->bodyType == (uint8_t)m2_dynamicBody || b->bodyType == (uint8_t)m2_dynamicBody; + bool lineA = a->type == (int32_t)m2_segmentShape || a->type == (int32_t)m2_chainSegmentShape; + bool lineB = b->type == (int32_t)m2_segmentShape || b->type == (int32_t)m2_chainSegmentShape; + if (!a->sensor && !b->sensor && (!dynamic || (lineA && lineB))) + { + return false; + } + if (a->group != 0 && a->group == b->group) + { + return a->group > 0; + } + return (a->category & b->mask) != 0 && (b->category & a->mask) != 0; } // Re-derive pairs touched by the moved set, then batch-merge with the // untouched remainder. // Which trees a moved shape queries: dynamic and kinematic movers sweep // every tree, static movers (teleports) only the dynamic one. -static bool MoverSeesTree(const m2World* world, int32_t shapeIndex, int32_t tree) +M2_NODISCARD static bool MoverSeesTree(const m2World* world, int32_t shapeIndex, int32_t tree) { return world->bodies.types[world->shapes.shapeBody[shapeIndex]] != (uint8_t)m2_staticBody || tree == (int32_t)m2_dynamicBody; @@ -72,7 +109,7 @@ static bool MoverSeesTree(const m2World* world, int32_t shapeIndex, int32_t tree // NOLINTNEXTLINE(readability-function-cognitive-complexity): rebuild, merge, diff: one pair stash void m2UpdatePairs(m2World* world) { - world->contacts.pairOverflow = 0; + world->contacts.pairsDropped = 0; if (world->broadphase.movedCount == 0) { return; @@ -82,6 +119,7 @@ void m2UpdatePairs(m2World* world) CompareI32); int32_t collected = 0; + int64_t dropped = 0; // every moved shape may meet every other: past 32 bits for (int32_t m = 0; m < world->broadphase.movedCount; ++m) { int32_t shapeIndex = world->broadphase.moved[m]; @@ -94,7 +132,8 @@ void m2UpdatePairs(m2World* world) m2Aabb fat = world->broadphase.treeNodes[treeIndex][world->broadphase.proxyIds[shapeIndex]].aabb; - bool moverDynamic = ShapeIsDynamic(world, shapeIndex); + m2PairSide moverSide = m2ShapePairSide(world, shapeIndex); + bool moverDynamic = moverSide.bodyType == (uint8_t)m2_dynamicBody; // Kinematic movers sweep every tree: a kinematic character // walking through a static trigger zone is the most ordinary // sensor story there is. Static movers only exist via @@ -124,52 +163,29 @@ void m2UpdatePairs(m2World* world) { continue; // same-body shapes never pair } - if (!moverDynamic && !ShapeIsDynamic(world, other) && - world->shapes.shapeSensor[shapeIndex] == 0 && - world->shapes.shapeSensor[other] == 0) - { - continue; // two non-dynamics only pair through a sensor - } - if (world->shapes.shapeSensor[shapeIndex] != 0 && - world->shapes.shapeSensor[other] != 0) + m2PairSide otherSide = m2ShapePairSide(world, other); + if (!m2SidesMayPair(&moverSide, &otherSide)) { - continue; // two sensors never detect each other - } - int32_t groupA = world->shapes.shapeGroup[shapeIndex]; - if (groupA != 0 && groupA == world->shapes.shapeGroup[other]) - { - if (groupA < 0) - { - continue; // same negative group: never collide - } - } - else if ((world->shapes.shapeCategory[shapeIndex] & - world->shapes.shapeMask[other]) == 0 || - (world->shapes.shapeCategory[other] & - world->shapes.shapeMask[shapeIndex]) == 0) - { - continue; // filtered out (category/mask, both ways) + continue; // their kinds or their filters keep them apart } if (m2JointsForbidPair(world, world->shapes.shapeBody[shapeIndex], world->shapes.shapeBody[other])) { continue; // connected without enableConnectedCollision } - if (collected < world->contacts.pairCapacity) + if (collected >= world->contacts.pairCapacity) { - world->contacts.pairScratch[collected] = PairKey(shapeIndex, other); + dropped += 1; + continue; } + world->contacts.pairScratch[collected] = PairKey(shapeIndex, other); collected += 1; } } } // A full pair table drops the excess candidates, counted loudly in - // m2Counters.pairOverflow rather than asserted away. - if (collected > world->contacts.pairCapacity) - { - world->contacts.pairOverflow += collected - world->contacts.pairCapacity; - collected = world->contacts.pairCapacity; - } + // m2Counters.pairsDropped rather than asserted away. + m2CountDrops(&world->contacts.pairsDropped, dropped); qsort(world->contacts.pairScratch, (size_t)collected, sizeof(uint64_t), CompareU64); @@ -228,7 +244,7 @@ void m2UpdatePairs(m2World* world) } else { - world->contacts.pairOverflow += 1; + m2CountDrops(&world->contacts.pairsDropped, 1); } } memcpy(world->contacts.pairKeys, world->contacts.pairMergeScratch, @@ -281,7 +297,10 @@ void m2UpdatePairs(m2World* world) } } } +} +void m2ClearMoved(m2World* world) +{ for (int32_t m = 0; m < world->broadphase.movedCount; ++m) { world->broadphase.inMoved[world->broadphase.moved[m]] = 0; diff --git a/third_party/maul2d/src/broadphase.h b/third_party/maul2d/src/broadphase.h index 2882eca29..1ef622d32 100644 --- a/third_party/maul2d/src/broadphase.h +++ b/third_party/maul2d/src/broadphase.h @@ -8,8 +8,29 @@ #include "world_internal.h" +// What the pair rules read of one shape. +typedef struct m2PairSide +{ + uint64_t category; + uint64_t mask; + int32_t group; + int32_t type; // m2ShapeType + bool sensor; + uint8_t bodyType; // m2BodyType of its body +} m2PairSide; + +m2PairSide m2ShapePairSide(const m2World* world, int32_t shapeIndex); + +// Whether the broadphase pairs two shapes of different bodies by their +// kinds, their bodies' types and their filters. The joints between their bodies have the last word +// (m2JointsForbidPair). +M2_NODISCARD bool m2SidesMayPair(const m2PairSide* a, const m2PairSide* b); + void m2RefilterJointedBodies(m2World* world, int32_t bodyA, int32_t bodyB); void m2UpdatePairs(m2World* world); +// Empties the moved list. The step calls it after the contact update, +// which reads the moved flags to tell which frozen pairs moved. +void m2ClearMoved(m2World* world); void m2PrunePairsOfShape(m2World* world, int32_t shapeIndex); #endif // MAUL2D_SRC_BROADPHASE_H diff --git a/third_party/maul2d/src/buoyancy.c b/third_party/maul2d/src/buoyancy.c index 0085c8ef8..ac422b1f2 100644 --- a/third_party/maul2d/src/buoyancy.c +++ b/third_party/maul2d/src/buoyancy.c @@ -45,7 +45,8 @@ static m2Vec2 Rotate(m2Rot q, m2Vec2 v) return (m2Vec2){q.c * v.x - q.s * v.y, q.s * v.x + q.c * v.y}; } -// What of a shape lies below the surface: area and world centroid. +// What of a shape lies below the surface: area and centroid, the centroid +// relative to the body's origin. typedef struct Immersion { float area; @@ -149,11 +150,12 @@ static float FanRadius(float r, int32_t m, float a, float sinA) return r * (sqrtf(sinA * sinA + k * (float)m * a) - sinA) / k; } -// The outline of a rounded convex polygon in the world: each corner fans -// from the previous edge's normal to the next one's in at least two -// steps of at most a sixteenth of a turn, keeping the shape's area. +// The outline of a rounded convex polygon, turned to the world about the +// body's origin: each corner fans from the previous edge's normal to the +// next one's in at least two steps of at most a sixteenth of a turn, +// keeping the shape's area. static int32_t RoundedOutline(const m2Vec2* verts, const m2Vec2* normals, int32_t count, - float radius, m2Transform xf, m2Vec2* out) + float radius, m2Rot q, m2Vec2* out) { int32_t n = 0; for (int32_t i = 0; i < count; ++i) @@ -176,65 +178,66 @@ static int32_t RoundedOutline(const m2Vec2* verts, const m2Vec2* normals, int32_ { float reach = k == 0 || k == steps ? radius : inner; m2Vec2 local = {verts[i].x + reach * dir.x, verts[i].y + reach * dir.y}; - m2Vec2 world = Rotate(xf.q, local); - out[n++] = (m2Vec2){world.x + (float)xf.p.x, world.y + (float)xf.p.y}; + out[n++] = Rotate(q, local); dir = k + 1 == steps ? to : Rotate(rot, dir); } } return n; } -static Immersion ImmerseShape(const m2World* world, int32_t shape, m2Transform xf, double surface) +// A shape against the surface, all of it relative to the body's origin: +// the outline only turned to the world, never moved to the body's world +// position, and the surface given as its height above that origin. In +// world coordinates, a body far from the world origin would sum its +// moments from large floats that cancel, and the force would depend on +// where in the world the body floats. +static Immersion ImmerseShape(const m2World* world, int32_t shape, m2Rot q, double surface) { const m2ShapeGeometry* g = &world->shapes.shapeGeometry[shape]; m2Vec2 outline[M2_OUTLINE_MAX]; int32_t n = 0; if (g->type == m2_circleShape) { - m2Vec2 c = Rotate(xf.q, g->circle.center); - return ImmerseCircle((m2Vec2){c.x + (float)xf.p.x, c.y + (float)xf.p.y}, g->circle.radius, - surface); + return ImmerseCircle(Rotate(q, g->circle.center), g->circle.radius, surface); } if (g->type == m2_capsuleShape) { m2Polygon core = m2MakeSegmentProxy(g->capsule.point1, g->capsule.point2, g->capsule.radius); - n = RoundedOutline(core.vertices, core.normals, 2, core.radius, xf, outline); + n = RoundedOutline(core.vertices, core.normals, 2, core.radius, q, outline); } else if (g->type == m2_polygonShape && g->polygon.radius > 0.0f) { n = RoundedOutline(g->polygon.vertices, g->polygon.normals, g->polygon.count, - g->polygon.radius, xf, outline); + g->polygon.radius, q, outline); } else if (g->type == m2_polygonShape) { for (int32_t i = 0; i < g->polygon.count; ++i) { - m2Vec2 w = Rotate(xf.q, g->polygon.vertices[i]); - outline[n++] = (m2Vec2){w.x + (float)xf.p.x, w.y + (float)xf.p.y}; + outline[n++] = Rotate(q, g->polygon.vertices[i]); } } return n >= 3 ? ImmerseOutline(outline, n, surface) : s_dry; } -m2FluidVolumeDef m2DefaultFluidVolumeDef(void) +m2WaterVolumeDef m2DefaultWaterVolumeDef(void) { - m2FluidVolumeDef def; + m2WaterVolumeDef def; memset(&def, 0, sizeof(def)); def.density = 2.0f; def.linearDrag = 1.0f; def.angularDrag = 0.5f; - def.internalValue = M2_FVOLUME_COOKIE; + def.internalValue = M2_WVOLUME_COOKIE; return def; } // The create record holds the def's fields and zero bytes between them: // copied field by field into the zeroed record, so no padding of the // caller's struct reaches the tape. -static void RecordFluidVolumeDef(m2FluidVolumeDef* out, const m2FluidVolumeDef* def) +static void RecordWaterVolumeDef(m2WaterVolumeDef* out, const m2WaterVolumeDef* def) { - out->regionLower = def->regionLower; - out->regionUpper = def->regionUpper; + out->bounds = def->bounds; out->surface = def->surface; out->density = def->density; out->linearDrag = def->linearDrag; @@ -244,25 +247,24 @@ static void RecordFluidVolumeDef(m2FluidVolumeDef* out, const m2FluidVolumeDef* out->internalValue = def->internalValue; } -static M2_NOINLINE m2Result CreateFluidVolumeWorker(m2World* world, const m2FluidVolumeDef* def, - m2FluidVolumeId* volumeOut) +static M2_NOINLINE m2Result CreateWaterVolumeWorker(m2World* world, const m2WaterVolumeDef* def, + m2WaterVolumeId* volumeOut) { if (volumeOut == NULL) { return m2Refuse(world, m2_errorInvalid); } - *volumeOut = m2_nullFluidVolumeId; + *volumeOut = m2_nullWaterVolumeId; m2Result writer = m2CheckWriter(world); if (writer != m2_success) { return writer; } - if (def == NULL || def->internalValue != M2_FVOLUME_COOKIE) + if (def == NULL || def->internalValue != M2_WVOLUME_COOKIE) { return m2Refuse(world, m2_errorInvalid); } - m2Result checked = m2CheckValid(m2FiniteD(def->surface) && m2FinitePos2(def->regionLower) && - m2FinitePos2(def->regionUpper)); + m2Result checked = m2CheckValid(m2FiniteD(def->surface) && m2WaterBoxValid(def->bounds)); checked = m2BothChecks(checked, m2CheckDensity(def->density)); checked = m2BothChecks(checked, m2CheckRate(def->linearDrag)); checked = m2BothChecks(checked, m2CheckRate(def->angularDrag)); @@ -271,109 +273,111 @@ static M2_NOINLINE m2Result CreateFluidVolumeWorker(m2World* world, const m2Flui { return m2Refuse(world, checked); } - if (world->volumes.fvFreeCount == 0) + if (world->volumes.wvFreeCount == 0) { return m2Refuse(world, m2_errorCapacity); // pool full: a runtime fact } - int32_t index = world->volumes.fvFreeQueue[world->volumes.fvFreeHead]; - world->volumes.fvFreeHead = (world->volumes.fvFreeHead + 1) % world->volumes.fvCapacity; - world->volumes.fvFreeCount -= 1; - if (index + 1 > world->volumes.maxFvIndex) - { - world->volumes.maxFvIndex = index + 1; - } - world->volumes.fvLower[index] = def->regionLower; - world->volumes.fvUpper[index] = def->regionUpper; - world->volumes.fvSurface[index] = def->surface; - world->volumes.fvDensity[index] = def->density; - world->volumes.fvLinearDrag[index] = def->linearDrag; - world->volumes.fvAngularDrag[index] = def->angularDrag; - world->volumes.fvFlow[index] = def->flow; - world->volumes.fvUserData[index] = def->userData; - world->volumes.fvAlive[index] = 1; - m2FluidVolumeId id = {(uint32_t)(index + 1), world->volumes.fvGenerations[index]}; + int32_t index = world->volumes.wvFreeQueue[world->volumes.wvFreeHead]; + world->volumes.wvFreeHead = (world->volumes.wvFreeHead + 1) % world->volumes.wvCapacity; + world->volumes.wvFreeCount -= 1; + if (index + 1 > world->volumes.maxWvIndex) + { + world->volumes.maxWvIndex = index + 1; + } + world->volumes.wvLower[index] = def->bounds.lowerBound; + world->volumes.wvUpper[index] = def->bounds.upperBound; + world->volumes.wvSurface[index] = def->surface; + world->volumes.wvDensity[index] = def->density; + world->volumes.wvLinearDrag[index] = def->linearDrag; + world->volumes.wvAngularDrag[index] = def->angularDrag; + world->volumes.wvFlow[index] = def->flow; + world->volumes.wvUserData[index] = def->userData; + world->volumes.wvAlive[index] = 1; + m2WaterVolumeId id = {(uint32_t)(index + 1), world->volumes.wvGenerations[index]}; if (world->recorder.journalActive != 0) { - m2OpCreateFluidVolume record; + m2OpCreateWaterVolume record; memset(&record, 0, sizeof(record)); - RecordFluidVolumeDef(&record.def, def); + RecordWaterVolumeDef(&record.def, def); record.expected = id; - m2JournalRecord(world, m2_opCreateFluidVolume, &record, (int32_t)sizeof(record)); + m2JournalRecord(world, m2_opCreateWaterVolume, &record, (int32_t)sizeof(record)); } *volumeOut = id; return m2_success; } -m2Result m2CreateFluidVolume(m2World* world, const m2FluidVolumeDef* def, - m2FluidVolumeId* volumeOut) +m2Result m2CreateWaterVolume(m2World* world, const m2WaterVolumeDef* def, + m2WaterVolumeId* volumeOut) { uint64_t env = m2FloatEnvEnter(); - m2Result result = CreateFluidVolumeWorker(world, def, volumeOut); + m2Result result = CreateWaterVolumeWorker(world, def, volumeOut); m2FloatEnvLeave(env); return result; } -static int32_t FvSlot(const m2World* world, m2FluidVolumeId id) +static int32_t WvSlot(const m2World* world, m2WaterVolumeId id) { int32_t index = (int32_t)id.index1 - 1; - if (world == NULL || index < 0 || index >= world->volumes.fvCapacity || - world->volumes.fvAlive[index] == 0 || world->volumes.fvGenerations[index] != id.generation) + if (world == NULL || index < 0 || index >= world->volumes.wvCapacity || + world->volumes.wvAlive[index] == 0 || world->volumes.wvGenerations[index] != id.generation) { return -1; } return index; } -static M2_NOINLINE m2Result DestroyFluidVolumeWorker(m2World* world, m2FluidVolumeId volumeId) +static M2_NOINLINE m2Result DestroyWaterVolumeWorker(m2World* world, m2WaterVolumeId volumeId) { m2Result writer = m2CheckWriter(world); if (writer != m2_success) { return writer; } - int32_t index = FvSlot(world, volumeId); + int32_t index = WvSlot(world, volumeId); if (index < 0) { return m2Refuse(world, m2_errorStale); } if (world->recorder.journalActive != 0) { - m2JournalRecord(world, m2_opDestroyFluidVolume, &volumeId, (int32_t)sizeof(volumeId)); + m2JournalRecord(world, m2_opDestroyWaterVolume, &volumeId, (int32_t)sizeof(volumeId)); } - world->volumes.fvAlive[index] = 0; - if (world->volumes.fvGenerations[index] == M2_LAST_GENERATION) + world->volumes.wvAlive[index] = 0; + if (world->volumes.wvGenerations[index] == M2_LAST_GENERATION) { - world->volumes.fvRetiredCount += 1; + world->volumes.wvRetiredCount += 1; return m2_success; } - world->volumes.fvGenerations[index] += 1; - world->volumes.fvFreeQueue[(world->volumes.fvFreeHead + world->volumes.fvFreeCount) % - world->volumes.fvCapacity] = index; - world->volumes.fvFreeCount += 1; + world->volumes.wvGenerations[index] += 1; + // In 64 bits: the water volume capacity has no bound that keeps head + // plus count under 2^31. + world->volumes.wvFreeQueue[((int64_t)world->volumes.wvFreeHead + world->volumes.wvFreeCount) % + world->volumes.wvCapacity] = index; + world->volumes.wvFreeCount += 1; return m2_success; } -m2Result m2DestroyFluidVolume(m2World* world, m2FluidVolumeId volumeId) +m2Result m2DestroyWaterVolume(m2World* world, m2WaterVolumeId volumeId) { uint64_t env = m2FloatEnvEnter(); - m2Result result = DestroyFluidVolumeWorker(world, volumeId); + m2Result result = DestroyWaterVolumeWorker(world, volumeId); m2FloatEnvLeave(env); return result; } -bool m2FluidVolume_IsValid(const m2World* world, m2FluidVolumeId volumeId) +bool m2WaterVolume_IsValid(const m2World* world, m2WaterVolumeId volumeId) { - return FvSlot(world, volumeId) >= 0; + return WvSlot(world, volumeId) >= 0; } -static M2_NOINLINE void FluidVolumeSetSurfaceWorker(m2World* world, m2FluidVolumeId volumeId, +static M2_NOINLINE void WaterVolumeSetSurfaceWorker(m2World* world, m2WaterVolumeId volumeId, double surface) { if (m2CheckWriter(world) != m2_success) { return; } - int32_t index = FvSlot(world, volumeId); + int32_t index = WvSlot(world, volumeId); if (index < 0 || !m2FiniteD(surface)) { m2Refuse(world, m2_errorInvalid); @@ -381,51 +385,74 @@ static M2_NOINLINE void FluidVolumeSetSurfaceWorker(m2World* world, m2FluidVolum } if (world->recorder.journalActive != 0) { - m2OpFluidSurface record; + m2OpWaterSurface record; memset(&record, 0, sizeof(record)); record.id = volumeId; record.surface = surface; - m2JournalRecord(world, m2_opSetFluidSurface, &record, (int32_t)sizeof(record)); + m2JournalRecord(world, m2_opSetWaterSurface, &record, (int32_t)sizeof(record)); } - world->volumes.fvSurface[index] = surface; + world->volumes.wvSurface[index] = surface; } -void m2FluidVolume_SetSurface(m2World* world, m2FluidVolumeId volumeId, double surface) +void m2WaterVolume_SetSurface(m2World* world, m2WaterVolumeId volumeId, double surface) { uint64_t env = m2FloatEnvEnter(); - FluidVolumeSetSurfaceWorker(world, volumeId, surface); + WaterVolumeSetSurfaceWorker(world, volumeId, surface); m2FloatEnvLeave(env); } -static M2_NOINLINE double FluidVolumeGetSurfaceWorker(const m2World* world, - m2FluidVolumeId volumeId) +static M2_NOINLINE double WaterVolumeGetSurfaceWorker(const m2World* world, + m2WaterVolumeId volumeId) { - int32_t index = FvSlot(world, volumeId); + int32_t index = WvSlot(world, volumeId); if (index < 0) { m2Refuse(world, m2_errorInvalid); return 0.0; } - return world->volumes.fvSurface[index]; + return world->volumes.wvSurface[index]; } -double m2FluidVolume_GetSurface(const m2World* world, m2FluidVolumeId volumeId) +double m2WaterVolume_GetSurface(const m2World* world, m2WaterVolumeId volumeId) { uint64_t env = m2FloatEnvEnter(); - double result = FluidVolumeGetSurfaceWorker(world, volumeId); + double result = WaterVolumeGetSurfaceWorker(world, volumeId); m2FloatEnvLeave(env); return result; } -uint64_t m2FluidVolume_GetUserData(const m2World* world, m2FluidVolumeId volumeId) +uint64_t m2WaterVolume_GetUserData(const m2World* world, m2WaterVolumeId volumeId) { - int32_t index = FvSlot(world, volumeId); + int32_t index = WvSlot(world, volumeId); if (index < 0) { m2Refuse(world, m2_errorInvalid); return 0; } - return world->volumes.fvUserData[index]; + return world->volumes.wvUserData[index]; +} + +void m2WaterVolume_SetUserData(m2World* world, m2WaterVolumeId volumeId, uint64_t userData) +{ + if (m2CheckWriter(world) != m2_success) + { + return; + } + int32_t index = WvSlot(world, volumeId); + if (index < 0) + { + m2Refuse(world, m2_errorInvalid); + return; + } + if (world->recorder.journalActive != 0) + { + m2OpWaterUserData record; + memset(&record, 0, sizeof(record)); + record.id = volumeId; + record.userData = userData; + m2JournalRecord(world, m2_opWaterUserData, &record, (int32_t)sizeof(record)); + } + world->volumes.wvUserData[index] = userData; } // The squared radius of gyration the angular drag takes, at most twice @@ -442,7 +469,7 @@ static void SaturateLoad(m2World* world, int32_t b) world->bodies.torques[b] = m2SaturateF(world->bodies.torques[b], M2_FORCE_LIMIT); } -static bool InWater(const m2World* world, int32_t b, int32_t v) +M2_NODISCARD static bool InWater(const m2World* world, int32_t b, int32_t v) { if (world->bodies.alive[b] == 0 || world->bodies.types[b] != (uint8_t)m2_dynamicBody || world->bodies.asleep[b] != 0 || world->bodies.disabled[b] != 0) @@ -450,8 +477,8 @@ static bool InWater(const m2World* world, int32_t b, int32_t v) return false; } m2Pos2 p = world->bodies.transforms[b].p; - m2Pos2 lo = world->volumes.fvLower[v]; - m2Pos2 hi = world->volumes.fvUpper[v]; + m2Pos2 lo = world->volumes.wvLower[v]; + m2Pos2 hi = world->volumes.wvUpper[v]; return p.x >= lo.x && p.x <= hi.x && p.y >= lo.y && p.y <= hi.y; } @@ -459,10 +486,11 @@ static bool InWater(const m2World* world, int32_t b, int32_t v) static void Soak(m2World* world, int32_t b, int32_t vol) { m2Transform xf = world->bodies.transforms[b]; + double surface = world->volumes.wvSurface[vol] - xf.p.y; // above the body's origin Immersion wet = s_dry; for (int32_t s = world->bodies.bodyShapeHead[b]; s != -1; s = world->shapes.shapeNext[s]) { - Immersion part = ImmerseShape(world, s, xf, world->volumes.fvSurface[vol]); + Immersion part = ImmerseShape(world, s, xf.q, surface); wet.area += part.area; wet.centroid.x += part.area * part.centroid.x; wet.centroid.y += part.area * part.centroid.y; @@ -472,13 +500,12 @@ static void Soak(m2World* world, int32_t b, int32_t vol) return; } m2Vec2 center = Rotate(xf.q, world->bodies.localCenters[b]); - m2Vec2 arm = {wet.centroid.x / wet.area - ((float)xf.p.x + center.x), - wet.centroid.y / wet.area - ((float)xf.p.y + center.y)}; + m2Vec2 arm = {wet.centroid.x / wet.area - center.x, wet.centroid.y / wet.area - center.y}; float w = world->bodies.angularVelocities[b]; m2Vec2 v = world->bodies.linearVelocities[b]; - m2Vec2 flow = world->volumes.fvFlow[vol]; - float lift = -world->volumes.fvDensity[vol] * wet.area; - float drag = -world->volumes.fvLinearDrag[vol] * wet.area; + m2Vec2 flow = world->volumes.wvFlow[vol]; + float lift = -world->volumes.wvDensity[vol] * wet.area; + float drag = -world->volumes.wvLinearDrag[vol] * wet.area; float fx = lift * world->gravity.x + drag * (v.x - w * arm.y - flow.x); float fy = lift * world->gravity.y + drag * (v.y + w * arm.x - flow.y); world->bodies.forces[b].x += fx; @@ -492,21 +519,21 @@ static void Soak(m2World* world, int32_t b, int32_t vol) // can make it as large as the inertia and mass ranges allow; no // body reaches past twice M2_LOCAL_LIMIT, so it is held to that. float gyration2 = m2MinF(invM / invI, M2_MAX_GYRATION2); - world->bodies.torques[b] -= gyration2 * wet.area * w * world->volumes.fvAngularDrag[vol]; + world->bodies.torques[b] -= gyration2 * wet.area * w * world->volumes.wvAngularDrag[vol]; } SaturateLoad(world, b); } // The per-step pass, volume by volume, bodies in slot order. -void m2ApplyFluidVolumes(m2World* world, float dt) +void m2ApplyWaterVolumes(m2World* world, float dt) { if (dt <= 0.0f) { return; } - for (int32_t v = 0; v < world->volumes.maxFvIndex; ++v) + for (int32_t v = 0; v < world->volumes.maxWvIndex; ++v) { - for (int32_t b = 0; world->volumes.fvAlive[v] != 0 && b < world->bodies.maxBodyIndex; ++b) + for (int32_t b = 0; world->volumes.wvAlive[v] != 0 && b < world->bodies.maxBodyIndex; ++b) { if (InWater(world, b, v)) { diff --git a/third_party/maul2d/src/buoyancy.h b/third_party/maul2d/src/buoyancy.h index 1fdd0f2ea..85fb9dabc 100644 --- a/third_party/maul2d/src/buoyancy.h +++ b/third_party/maul2d/src/buoyancy.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2026 Sirac Ozmen // -// Fluid volumes and wind: forces from water regions and moving air. +// Water volumes and wind: forces from water regions and moving air. #ifndef MAUL2D_SRC_BUOYANCY_H #define MAUL2D_SRC_BUOYANCY_H @@ -9,11 +9,22 @@ #include "world_internal.h" // A def is valid only when its internalValue matches its cookie. -#define M2_FVOLUME_COOKIE (M2_COOKIE ^ ((int32_t)sizeof(m2FluidVolumeDef) << 8) ^ 17) +#define M2_WVOLUME_COOKIE (M2_COOKIE ^ ((int32_t)sizeof(m2WaterVolumeDef) << 8) ^ 17) -// Per-step forces on bodies: buoyancy and drag in the fluid volumes, +// A water volume's box: finite, with its upper corner above its lower one +// on both axes. A box without area holds no body's origin, so its volume +// would never act. m2CreateWaterVolume takes only such a box, no call +// changes it later, and a restore holds every live volume to the same +// test. +M2_NODISCARD static inline bool m2WaterBoxValid(m2WorldAabb box) +{ + return m2ValidWorldAabb(box) && box.lowerBound.x < box.upperBound.x && + box.lowerBound.y < box.upperBound.y; +} + +// Per-step forces on bodies: buoyancy and drag in the water volumes, // then the ambient wind. -void m2ApplyFluidVolumes(m2World* world, float dt); +void m2ApplyWaterVolumes(m2World* world, float dt); void m2ApplyWind(m2World* world, float dt); #endif // MAUL2D_SRC_BUOYANCY_H diff --git a/third_party/maul2d/src/ccd.c b/third_party/maul2d/src/ccd.c index be257dce1..383affff6 100644 --- a/third_party/maul2d/src/ccd.c +++ b/third_party/maul2d/src/ccd.c @@ -3,11 +3,12 @@ // // Bullet continuous collision: conservative advancement // per substep for bullet-flagged bodies. Deterministic by construction: -// bullets advance in body-index order, candidates come from tree queries -// filtered and processed in canonical shape order, iteration counts are -// capped, and hitting the cap is documented behavior (slight overlap -// next substep, which the speculative solver then resolves). Bullets do -// not sweep against other bullets. +// bullets advance in body-index order, every shape in a bullet's sweep is +// advanced against, and the earliest impact over them does not depend on +// the order the trees yield them; iteration counts are capped, and +// hitting the cap is documented behavior (slight overlap next substep, +// which the speculative solver then resolves). Bullets do not sweep +// against other bullets. // // Simplifications: the bullet sweeps as its bounding circle (exact for // circle shapes, conservative for the rest), and rotation during the @@ -21,7 +22,6 @@ #define M2_TOI_ITERATIONS 20 #define M2_TOI_TARGET 0.005f // stop one slop short of the surface -#define M2_TOI_CANDIDATES 64 static float BulletBoundingRadius(const m2World* world, int32_t body) { @@ -88,7 +88,7 @@ static float DistanceToShape(const m2World* world, int32_t shape, m2Pos2 wp, flo // Whether a shape the sweep overlaps may stop the bullet body: not a // sensor, not on the ghost side of a one-way chain, not the bullet itself // or another bullet, and allowed by the collision filters. -static bool BulletMayHit(const m2World* world, int32_t body, int32_t shape) +M2_NODISCARD static bool BulletMayHit(const m2World* world, int32_t body, int32_t shape) { if (world->shapes.shapeSensor[shape] != 0) { @@ -137,27 +137,30 @@ static bool BulletMayHit(const m2World* world, int32_t body, int32_t shape) return mayCollide; } -// The lowest M2_TOI_CANDIDATES shape slots in the sweep that may stop the -// bullet, in ascending order whatever the tree's shape. Returns the count. -static int32_t SweepCandidates(const m2World* world, int32_t body, m2Aabb sweep, - int32_t* candidates) +// Conservative advancement of a bullet's move against one shape: the +// time of impact if it comes before minT, else minT. A shape's own steps +// never depend on the others, so the earliest impact over a sweep's shapes +// is the same in whatever order the trees yield them. +static float EarliestImpact(const m2World* world, int32_t shape, m2Pos2 p0, double mx, double my, + float moveLen, float bulletRadius, float minT) { - int32_t count = 0; - for (int32_t t = 0; t < M2_TREE_COUNT; ++t) + float t = 0.0f; + for (int32_t iter = 0; iter < M2_TOI_ITERATIONS; ++iter) { - m2TreeCursor cursor; - m2TreeBeginQuery(&cursor, &world->broadphase.trees[t], world->broadphase.treeNodes[t], - sweep); - int32_t shape; - while (m2TreeNextQuery(&cursor, &shape)) + m2Pos2 wp = {p0.x + (double)t * mx, p0.y + (double)t * my}; + float distance = DistanceToShape(world, shape, wp, bulletRadius); + if (distance < M2_TOI_TARGET) { - if (BulletMayHit(world, body, shape)) - { - m2TreeKeepLowest(candidates, &count, M2_TOI_CANDIDATES, shape); - } + return t < minT ? t : minT; + } + float advance = (distance - 0.5f * M2_TOI_TARGET) / moveLen; + t += advance; + if (t >= minT || t >= 1.0f) + { + return minT; // no earlier impact on this shape } } - return count; + return minT; } // Sweep one bullet from p0 toward its current position; clamp on impact. @@ -180,35 +183,20 @@ static void SweepBullet(m2World* world, int32_t body, m2Pos2 p0) sweep.upperBound.x = (p0.x > p1.x ? p0.x : p1.x) + (double)bulletRadius; sweep.upperBound.y = (p0.y > p1.y ? p0.y : p1.y) + (double)bulletRadius; - int32_t candidates[M2_TOI_CANDIDATES]; - int32_t candidateCount = SweepCandidates(world, body, sweep, candidates); - if (candidateCount == 0) - { - return; - } - - // Conservative advancement to the earliest impact across candidates. + // Conservative advancement to the earliest impact across every shape + // in the sweep that may stop the bullet. float minT = 1.0f; - for (int32_t c = 0; c < candidateCount; ++c) + for (int32_t t = 0; t < M2_TREE_COUNT; ++t) { - float t = 0.0f; - for (int32_t iter = 0; iter < M2_TOI_ITERATIONS; ++iter) + m2TreeCursor cursor; + m2TreeBeginQuery(&cursor, &world->broadphase.trees[t], world->broadphase.treeNodes[t], + sweep); + int32_t shape; + while (m2TreeNextQuery(&cursor, &shape)) { - m2Pos2 wp = {p0.x + (double)t * mx, p0.y + (double)t * my}; - float distance = DistanceToShape(world, candidates[c], wp, bulletRadius); - if (distance < M2_TOI_TARGET) - { - if (t < minT) - { - minT = t; - } - break; - } - float advance = (distance - 0.5f * M2_TOI_TARGET) / moveLen; - t += advance; - if (t >= minT || t >= 1.0f) + if (BulletMayHit(world, body, shape)) { - break; // no earlier impact on this candidate + minT = EarliestImpact(world, shape, p0, mx, my, moveLen, bulletRadius, minT); } } } diff --git a/third_party/maul2d/src/chain.c b/third_party/maul2d/src/chain.c index 5e5e92a74..022c37aab 100644 --- a/third_party/maul2d/src/chain.c +++ b/third_party/maul2d/src/chain.c @@ -159,22 +159,16 @@ static M2_NOINLINE m2Result CreateChainWorker(m2World* world, m2BodyId bodyId, world->chains.chainFreeHead = (world->chains.chainFreeHead + 1) % world->shapes.shapeCapacity; world->chains.chainFreeCount -= 1; + // The first segment always fits: the pool has a free slot, the def + // check covers every field a shape create checks, and a tree insert + // cannot fail (ApplyWorldDef sizes the node pools). int32_t created = CreateSegments(world, bodyId, def, chainIndex); - if (created == 0) - { - // Unreachable while the shape pool had room: hand the claimed slot - // back to the head of the free ring, generation untouched, since a - // refused create is not journaled and must move no later id. - world->chains.chainFreeHead = - (world->chains.chainFreeHead + world->shapes.shapeCapacity - 1) % - world->shapes.shapeCapacity; - world->chains.chainFreeQueue[world->chains.chainFreeHead] = chainIndex; - world->chains.chainFreeCount += 1; - return m2Refuse(world, m2_errorCapacity); - } + M2_ASSERT(created > 0); world->chains.chainAlive[chainIndex] = 1; world->chains.chainBody[chainIndex] = bodyIndex; world->chains.chainLoop[chainIndex] = def->isLoop ? 1 : 0; + world->chains.chainFriction[chainIndex] = def->friction; + world->chains.chainRestitution[chainIndex] = def->restitution; if (chainIndex + 1 > world->chains.maxChainIndex) { world->chains.maxChainIndex = chainIndex + 1; @@ -306,6 +300,14 @@ static void ChainMaterialInternal(m2World* world, m2ChainId chainId, int32_t cha record.value = value; m2JournalRecord(world, op, &record, (int32_t)sizeof(record)); } + if (op == m2_opChainFriction) + { + world->chains.chainFriction[chainIndex] = value; + } + else + { + world->chains.chainRestitution[chainIndex] = value; + } int32_t body = world->chains.chainBody[chainIndex]; for (int32_t s = world->bodies.bodyShapeHead[body]; s != -1; s = world->shapes.shapeNext[s]) { @@ -369,13 +371,41 @@ void m2Chain_SetRestitution(m2World* world, m2ChainId chainId, float restitution m2FloatEnvLeave(env); } -int32_t m2Chain_GetShapes(const m2World* world, m2ChainId chainId, m2ShapeId* ids, int32_t capacity) +static M2_NOINLINE float ChainGetMaterialWorker(const m2World* world, m2ChainId chainId, + bool friction) { - int32_t chainIndex = world != NULL ? ChainSlot(world, chainId) : -1; - if (chainIndex < 0) + int32_t index = world != NULL ? ChainSlot(world, chainId) : -1; + if (index < 0) { m2Refuse(world, m2_errorInvalid); - return 0; + return 0.0f; + } + return friction ? world->chains.chainFriction[index] : world->chains.chainRestitution[index]; +} + +float m2Chain_GetFriction(const m2World* world, m2ChainId chainId) +{ + uint64_t env = m2FloatEnvEnter(); + float result = ChainGetMaterialWorker(world, chainId, true); + m2FloatEnvLeave(env); + return result; +} + +float m2Chain_GetRestitution(const m2World* world, m2ChainId chainId) +{ + uint64_t env = m2FloatEnvEnter(); + float result = ChainGetMaterialWorker(world, chainId, false); + m2FloatEnvLeave(env); + return result; +} + +int32_t m2Chain_GetShapes(const m2World* world, m2ChainId chainId, m2ShapeId* idsOut, + int32_t capacity) +{ + int32_t chainIndex = world != NULL ? ChainSlot(world, chainId) : -1; + if (chainIndex < 0 || !m2ListOutputValid(idsOut, capacity)) + { + return m2RefuseList(world, idsOut, capacity, sizeof(*idsOut)); } int32_t total = 0; for (int32_t i = 0; i < world->shapes.maxShapeIndex; ++i) @@ -384,9 +414,9 @@ int32_t m2Chain_GetShapes(const m2World* world, m2ChainId chainId, m2ShapeId* id { continue; } - if (ids != NULL && total < capacity) + if (idsOut != NULL && total < capacity) { - ids[total] = m2MakeShapeId(world, i); + idsOut[total] = m2MakeShapeId(world, i); } total += 1; } diff --git a/third_party/maul2d/src/contact.c b/third_party/maul2d/src/contact.c index 580237dc7..3161ac1cc 100644 --- a/third_party/maul2d/src/contact.c +++ b/third_party/maul2d/src/contact.c @@ -111,12 +111,8 @@ void m2EmitSensorBegin(m2World* world, int32_t shapeA, int32_t shapeB) // --- Contacts ------------------------------------------------------- -// Relative pose of shape B's body in shape A's body frame: the single -// f64 -> f32 crossing for the contact stage. -static m2RelativePose MakeRelativePose(const m2World* world, int32_t bodyA, int32_t bodyB) +m2RelativePose m2MakeRelativePose(m2Transform xfA, m2Transform xfB) { - m2Transform xfA = world->bodies.transforms[bodyA]; - m2Transform xfB = world->bodies.transforms[bodyB]; float dx = (float)(xfB.p.x - xfA.p.x); float dy = (float)(xfB.p.y - xfA.p.y); m2RelativePose pose; @@ -204,22 +200,20 @@ static void ApplyChainLaws(m2Manifold* manifold, const m2ChainSegment* chain) manifold->pointCount = kept; } -static m2Manifold ComputeManifoldRaw(const m2World* world, int32_t shapeA, int32_t shapeB, +static m2Manifold ComputeManifoldRaw(const m2ShapeGeometry* ga, const m2ShapeGeometry* gb, m2RelativePose pose); -static m2Manifold ComputeManifold(const m2World* world, int32_t shapeA, int32_t shapeB, - m2RelativePose pose) +m2Manifold m2ComputeManifold(const m2ShapeGeometry* ga, const m2ShapeGeometry* gb, + m2RelativePose pose) { - const m2ShapeGeometry* ga = &world->shapes.shapeGeometry[shapeA]; - const m2ShapeGeometry* gb = &world->shapes.shapeGeometry[shapeB]; if (gb->type == m2_chainSegmentShape && ga->type != m2_chainSegmentShape) { // Canonical: the chain plays shape A so its laws apply in its // own frame; the manifold flips back on the way out. - m2Manifold m = ComputeManifold(world, shapeB, shapeA, InvertPose(pose)); + m2Manifold m = m2ComputeManifold(gb, ga, InvertPose(pose)); return FlipManifold(m, pose); } - m2Manifold m = ComputeManifoldRaw(world, shapeA, shapeB, pose); + m2Manifold m = ComputeManifoldRaw(ga, gb, pose); if (ga->type == m2_chainSegmentShape) { ApplyChainLaws(&m, &ga->chainSegment); @@ -227,11 +221,9 @@ static m2Manifold ComputeManifold(const m2World* world, int32_t shapeA, int32_t return m; } -static m2Manifold ComputeManifoldRaw(const m2World* world, int32_t shapeA, int32_t shapeB, +static m2Manifold ComputeManifoldRaw(const m2ShapeGeometry* ga, const m2ShapeGeometry* gb, m2RelativePose pose) { - const m2ShapeGeometry* ga = &world->shapes.shapeGeometry[shapeA]; - const m2ShapeGeometry* gb = &world->shapes.shapeGeometry[shapeB]; if (ga->type == m2_circleShape && gb->type == m2_circleShape) { @@ -322,6 +314,20 @@ typedef struct m2UpdateContactsCtx m2World* world; } m2UpdateContactsCtx; +// A frozen end: not moved since the last step, and static or asleep for +// two step ends, so its transform and geometry are those its manifolds +// were computed from. A kinematic end never freezes (its velocity can +// change without stepping). A host moves a static shape without waking +// the sleepers it does not touch, so a moved shape thaws its pairs. +M2_NODISCARD static bool ShapeIsFrozen(const m2World* world, int32_t shape) +{ + int32_t body = world->shapes.shapeBody[shape]; + return world->broadphase.inMoved[shape] == 0 && + (world->bodies.types[body] == (uint8_t)m2_staticBody || + (world->bodies.types[body] == (uint8_t)m2_dynamicBody && + world->bodies.asleep[body] != 0 && world->bodies.sleepStreak[body] >= 2)); +} + // Each pair writes only its own manifold slot, so the range splits // freely across workers without touching the arithmetic. // NOLINTNEXTLINE(readability-function-cognitive-complexity): per-pair manifold kernel @@ -333,22 +339,10 @@ static void UpdateContactsRange(int32_t begin, int32_t end, void* userCtx) int32_t shapeA = (int32_t)(world->contacts.pairKeys[i] >> 32); int32_t shapeB = (int32_t)(world->contacts.pairKeys[i] & 0xFFFFFFFFu); - // Frozen pair: both ends static or sleeping, so transforms and - // geometry are untouched and the stored manifold is exactly - // what ComputeManifold would return - skip the arithmetic, - // keep the bits. A kinematic end never freezes (its velocity - // can change without stepping). - int32_t frozenBodyA = world->shapes.shapeBody[shapeA]; - int32_t frozenBodyB = world->shapes.shapeBody[shapeB]; - bool frozenA = - world->bodies.types[frozenBodyA] == (uint8_t)m2_staticBody || - (world->bodies.types[frozenBodyA] == (uint8_t)m2_dynamicBody && - world->bodies.asleep[frozenBodyA] != 0 && world->bodies.sleepStreak[frozenBodyA] >= 2); - bool frozenB = - world->bodies.types[frozenBodyB] == (uint8_t)m2_staticBody || - (world->bodies.types[frozenBodyB] == (uint8_t)m2_dynamicBody && - world->bodies.asleep[frozenBodyB] != 0 && world->bodies.sleepStreak[frozenBodyB] >= 2); - if (frozenA && frozenB) + // Frozen pair: the stored manifold is exactly what + // m2ComputeManifold would return - skip the arithmetic, keep the + // bits. + if (ShapeIsFrozen(world, shapeA) && ShapeIsFrozen(world, shapeB)) { int32_t flo = 0; int32_t fhi = world->contacts.oldPairCount - 1; @@ -384,9 +378,11 @@ static void UpdateContactsRange(int32_t begin, int32_t end, void* userCtx) // fall through and compute once. } - m2RelativePose pose = MakeRelativePose(world, world->shapes.shapeBody[shapeA], - world->shapes.shapeBody[shapeB]); - m2Manifold fresh = ComputeManifold(world, shapeA, shapeB, pose); + m2RelativePose pose = + m2MakeRelativePose(world->bodies.transforms[world->shapes.shapeBody[shapeA]], + world->bodies.transforms[world->shapes.shapeBody[shapeB]]); + m2Manifold fresh = m2ComputeManifold(&world->shapes.shapeGeometry[shapeA], + &world->shapes.shapeGeometry[shapeB], pose); // Locate the previous manifold for this pair (both lists sorted; // binary search keeps this O(P log P) worst case). @@ -505,21 +501,18 @@ static M2_NOINLINE int32_t ShapeGetSensorOverlapsWorker(const m2World* world, m2ShapeId sensorShapeId, m2ShapeId* overlaps, int32_t capacity) { - if (world == NULL) + if (world == NULL || !m2ListOutputValid(overlaps, capacity)) { - return 0; + return m2RefuseList(world, overlaps, capacity, sizeof(*overlaps)); } + // A stale id and a solid shape, the wrong kind, are refused alike. int32_t sensor = (int32_t)sensorShapeId.index1 - 1; if (sensor < 0 || sensor >= world->shapes.shapeCapacity || world->shapes.shapeAlive[sensor] == 0 || - world->shapes.shapeGenerations[sensor] != sensorShapeId.generation) - { - m2Refuse(world, m2_errorInvalid); - return 0; - } - if (world->shapes.shapeSensor[sensor] == 0) + world->shapes.shapeGenerations[sensor] != sensorShapeId.generation || + world->shapes.shapeSensor[sensor] == 0) { - return 0; // a solid shape has no overlaps to list + return m2RefuseList(world, overlaps, capacity, sizeof(*overlaps)); } int32_t total = 0; for (int32_t i = 0; i < world->contacts.pairCount; ++i) @@ -544,20 +537,42 @@ static M2_NOINLINE int32_t ShapeGetSensorOverlapsWorker(const m2World* world, } int32_t m2Shape_GetSensorOverlaps(const m2World* world, m2ShapeId sensorShapeId, - m2ShapeId* overlaps, int32_t capacity) + m2ShapeId* overlapsOut, int32_t capacity) { uint64_t env = m2FloatEnvEnter(); - int32_t result = ShapeGetSensorOverlapsWorker(world, sensorShapeId, overlaps, capacity); + int32_t result = ShapeGetSensorOverlapsWorker(world, sensorShapeId, overlapsOut, capacity); m2FloatEnvLeave(env); return result; } +// Writes the public view of touching pair i: its shapes, the world +// normal, and each live point's separation and impulses. +static void FillContactData(const m2World* world, int32_t i, m2ContactData* out) +{ + int32_t shapeA = (int32_t)(world->contacts.pairKeys[i] >> 32); + int32_t shapeB = (int32_t)(world->contacts.pairKeys[i] & 0xFFFFFFFFu); + const m2Manifold* manifold = &world->contacts.manifolds[i]; + out->shapeIdA = m2MakeShapeId(world, shapeA); + out->shapeIdB = m2MakeShapeId(world, shapeB); + m2Rot qA = world->bodies.transforms[world->shapes.shapeBody[shapeA]].q; + out->normal = (m2Vec2){qA.c * manifold->normal.x - qA.s * manifold->normal.y, + qA.s * manifold->normal.x + qA.c * manifold->normal.y}; + out->pointCount = manifold->pointCount; + for (int32_t k = 0; k < 2; ++k) + { + bool live = k < manifold->pointCount; + out->separations[k] = live ? manifold->points[k].separation : 0.0f; + out->normalImpulses[k] = live ? manifold->points[k].normalImpulse : 0.0f; + out->tangentImpulses[k] = live ? manifold->points[k].tangentImpulse : 0.0f; + } +} + static M2_NOINLINE int32_t WorldGetContactDataWorker(const m2World* world, m2ContactData* data, int32_t capacity) { - if (world == NULL) + if (world == NULL || !m2ListOutputValid(data, capacity)) { - return 0; + return m2RefuseList(world, data, capacity, sizeof(*data)); } int32_t total = 0; for (int32_t i = 0; i < world->contacts.pairCount; ++i) @@ -576,33 +591,17 @@ static M2_NOINLINE int32_t WorldGetContactDataWorker(const m2World* world, m2Con } if (data != NULL && total < capacity) { - int32_t shapeA = (int32_t)(world->contacts.pairKeys[i] >> 32); - int32_t shapeB = (int32_t)(world->contacts.pairKeys[i] & 0xFFFFFFFFu); - m2Manifold* manifold = &world->contacts.manifolds[i]; - m2ContactData* out = data + total; - out->shapeIdA = m2MakeShapeId(world, shapeA); - out->shapeIdB = m2MakeShapeId(world, shapeB); - m2Rot qA = world->bodies.transforms[world->shapes.shapeBody[shapeA]].q; - out->normal = (m2Vec2){qA.c * manifold->normal.x - qA.s * manifold->normal.y, - qA.s * manifold->normal.x + qA.c * manifold->normal.y}; - out->pointCount = manifold->pointCount; - for (int32_t k = 0; k < 2; ++k) - { - bool live = k < manifold->pointCount; - out->separations[k] = live ? manifold->points[k].separation : 0.0f; - out->normalImpulses[k] = live ? manifold->points[k].normalImpulse : 0.0f; - out->tangentImpulses[k] = live ? manifold->points[k].tangentImpulse : 0.0f; - } + FillContactData(world, i, data + total); } total += 1; } return total; } -int32_t m2World_GetContactData(const m2World* world, m2ContactData* data, int32_t capacity) +int32_t m2World_GetContactData(const m2World* world, m2ContactData* dataOut, int32_t capacity) { uint64_t env = m2FloatEnvEnter(); - int32_t result = WorldGetContactDataWorker(world, data, capacity); + int32_t result = WorldGetContactDataWorker(world, dataOut, capacity); m2FloatEnvLeave(env); return result; } diff --git a/third_party/maul2d/src/contact.h b/third_party/maul2d/src/contact.h index 3b85a681b..0f3cd6e23 100644 --- a/third_party/maul2d/src/contact.h +++ b/third_party/maul2d/src/contact.h @@ -13,6 +13,16 @@ void m2EmitBegin(m2World* world, int32_t shapeA, int32_t shapeB, int32_t pairInd void m2EmitSensorEnd(m2World* world, int32_t shapeA, int32_t shapeB); void m2EmitSensorBegin(m2World* world, int32_t shapeA, int32_t shapeB); void m2StashContacts(m2World* world); + +// Relative pose of body B in body A's frame: the single f64 -> f32 +// crossing for the contact stage. +m2RelativePose m2MakeRelativePose(m2Transform xfA, m2Transform xfB); + +// The manifold of geometry ga, in a pair's lower slot, and gb, posed by +// `pose` in ga's body frame: what the step computes for the pair before +// the impulses carry over. +m2Manifold m2ComputeManifold(const m2ShapeGeometry* ga, const m2ShapeGeometry* gb, + m2RelativePose pose); void m2UpdateContacts(m2World* world); #endif // MAUL2D_SRC_CONTACT_H diff --git a/third_party/maul2d/src/contact_solver.c b/third_party/maul2d/src/contact_solver.c index b2e2259fd..dc2dbf833 100644 --- a/third_party/maul2d/src/contact_solver.c +++ b/third_party/maul2d/src/contact_solver.c @@ -37,7 +37,7 @@ size_t m2ContactBlockScratchBytes(int32_t pairCapacity) return ckd_mul(&bytes, blocks, sizeof(m2ContactBlock)) ? SIZE_MAX : bytes; } -static bool IsDynamic(const m2World* world, int32_t body) +M2_NODISCARD static bool IsDynamic(const m2World* world, int32_t body) { return world->bodies.types[body] == (uint8_t)m2_dynamicBody; } @@ -114,7 +114,7 @@ static void PrepareMaterial(const m2World* world, m2ContactConstraint* c, int32_ // True when the pair is solved this step: no sensor, something can move, // and at least one side is awake. -static bool PairSolves(const m2World* world, int32_t shapeA, int32_t shapeB) +M2_NODISCARD static bool PairSolves(const m2World* world, int32_t shapeA, int32_t shapeB) { if (world->shapes.shapeSensor[shapeA] != 0 || world->shapes.shapeSensor[shapeB] != 0) { diff --git a/third_party/maul2d/src/core.c b/third_party/maul2d/src/core.c index 7d38a892c..220fc111e 100644 --- a/third_party/maul2d/src/core.c +++ b/third_party/maul2d/src/core.c @@ -12,7 +12,7 @@ m2Version m2GetVersion(void) return (m2Version){M2_VERSION_MAJOR, M2_VERSION_MINOR, M2_VERSION_PATCH}; } -const char* m2ResultName(m2Result result) +const char* m2GetResultName(m2Result result) { switch (result) { diff --git a/third_party/maul2d/src/core.h b/third_party/maul2d/src/core.h index 52a714d8d..82c555845 100644 --- a/third_party/maul2d/src/core.h +++ b/third_party/maul2d/src/core.h @@ -12,6 +12,13 @@ #include "maul2d/core_math.h" #include +#include + +#if defined(_MSC_VER) && defined(_M_X64) +#include +#elif defined(_MSC_VER) && defined(_M_ARM64) +#include +#endif // M2_ASSERT states an internal invariant: a state that cannot occur // unless the library itself is wrong. Caller input is never asserted; it @@ -26,22 +33,90 @@ #define M2_ASSERT(cond) ((cond) ? (void)0 : __builtin_trap()) #endif +// Pinned minimum: exactly (a < b ? a : b), in this operand order, +// including signed zero and NaN behavior. MSVC folds the plain ternary +// under value numbering (+0/-0 compare equal), so on MSVC x64 this is +// implemented with scalar MINSS, whose ISA-defined semantics are exactly +// the pinned form. NEON FMIN has different semantics and must never be +// used here; GCC/Clang compile the ternary faithfully (CI-verified). +static inline float m2MinF(float a, float b) +{ +#if defined(_MSC_VER) && defined(_M_X64) + return _mm_cvtss_f32(_mm_min_ss(_mm_set_ss(a), _mm_set_ss(b))); +#elif defined(_MSC_VER) && defined(_M_ARM64) + // The plain ternary got folded away from the pinned zero-sign + // semantics (the arm64 cell caught it on its first run), so the + // select is explicit NEON: compare into a mask, bit-select the + // lanes, never fminnm. + float32x2_t va = vdup_n_f32(a); + float32x2_t vb = vdup_n_f32(b); + return vget_lane_f32(vbsl_f32(vclt_f32(va, vb), va, vb), 0); +#elif defined(_MSC_VER) +#error \ + "MSVC on this target: pinned min/max semantics unproven; implement with verified intrinsics first" +#else + return a < b ? a : b; +#endif +} + +// Pinned maximum: exactly (a > b ? a : b), in this operand order. +// See m2MinF for the MSVC note (scalar MAXSS matches the pinned form). +static inline float m2MaxF(float a, float b) +{ +#if defined(_MSC_VER) && defined(_M_X64) + return _mm_cvtss_f32(_mm_max_ss(_mm_set_ss(a), _mm_set_ss(b))); +#elif defined(_MSC_VER) && defined(_M_ARM64) + // See m2MinF: explicit NEON compare and bit-select. + float32x2_t va = vdup_n_f32(a); + float32x2_t vb = vdup_n_f32(b); + return vget_lane_f32(vbsl_f32(vcgt_f32(va, vb), va, vb), 0); +#elif defined(_MSC_VER) +#error \ + "MSVC on this target: pinned min/max semantics unproven; implement with verified intrinsics first" +#else + return a > b ? a : b; +#endif +} + +// Absolute value, pinned to IEEE |x| semantics: the sign bit is cleared, +// so m2AbsF(-0.0f) == +0.0f on every platform. Implemented as a bit mask +// because a ternary abs is compiler-foldable into divergent signed-zero +// behavior (MSVC emits a sign mask, GCC/Clang keep the branch: a real +// cross-cell hash break, caught by the gate). +static inline float m2AbsF(float a) +{ + union + { + float f; + uint32_t u; + } bits; + bits.f = a; + bits.u &= 0x7FFFFFFFu; + return bits.f; +} + +// Clamp to [lo, hi] using the pinned min and max. +static inline float m2ClampF(float a, float lo, float hi) +{ + return m2MaxF(lo, m2MinF(a, hi)); +} + // Finite checks for caller input and invariants. NaN compares false // and infinity minus infinity is NaN, so x - x == 0 refuses NaN and // both infinities in one test without libm. -static inline bool m2FiniteF(float x) +M2_NODISCARD static inline bool m2FiniteF(float x) { return x - x == 0.0f; // NOLINT(misc-redundant-expression): the finite test } -static inline bool m2FiniteD(double x) +M2_NODISCARD static inline bool m2FiniteD(double x) { return x - x == 0.0; // NOLINT(misc-redundant-expression): the finite test } -static inline bool m2FiniteVec2(m2Vec2 v) +M2_NODISCARD static inline bool m2FiniteVec2(m2Vec2 v) { return m2FiniteF(v.x) && m2FiniteF(v.y); } -static inline bool m2FinitePos2(m2Pos2 p) +M2_NODISCARD static inline bool m2FinitePos2(m2Pos2 p) { return m2FiniteD(p.x) && m2FiniteD(p.y); } @@ -175,6 +250,12 @@ static inline m2Result m2CheckMaxForce(float x) { return m2CheckNonNegative(x, M2_FORCE_LIMIT); } +// A body's sleep threshold as a caller gives it: a resting speed, 0 for +// the default. +static inline m2Result m2CheckSleepThreshold(float x) +{ + return m2CheckNonNegative(x, M2_SPEED_LIMIT); +} static inline m2Result m2CheckImpulse(float x) { return m2CheckWithin(x, M2_IMPULSE_LIMIT); @@ -224,107 +305,107 @@ static inline m2Result m2CheckBreakLimit(float x) // The same checks as a yes or no, for restore, replay and the setters // that return nothing. -static inline bool m2LocalVec2(m2Vec2 v) +M2_NODISCARD static inline bool m2LocalVec2(m2Vec2 v) { return m2CheckLocalVec2(v) == m2_success; } -static inline bool m2LocalLength(float x) +M2_NODISCARD static inline bool m2LocalLength(float x) { return m2CheckLocalLength(x) == m2_success; } -static inline bool m2WithinF(float x, float lo, float hi) +M2_NODISCARD static inline bool m2WithinF(float x, float lo, float hi) { return x >= lo && x <= hi; } -static inline bool m2ValidSpeed(float x) +M2_NODISCARD static inline bool m2ValidSpeed(float x) { return m2CheckSpeed(x) == m2_success; } -static inline bool m2ValidVelocity(m2Vec2 v) +M2_NODISCARD static inline bool m2ValidVelocity(m2Vec2 v) { return m2CheckVelocity(v) == m2_success; } -static inline bool m2ValidAcceleration(m2Vec2 a) +M2_NODISCARD static inline bool m2ValidAcceleration(m2Vec2 a) { return m2CheckAcceleration(a) == m2_success; } -static inline bool m2ValidGravityScale(float x) +M2_NODISCARD static inline bool m2ValidGravityScale(float x) { return m2CheckGravityScale(x) == m2_success; } -static inline bool m2ValidRate(float x) +M2_NODISCARD static inline bool m2ValidRate(float x) { return m2CheckRate(x) == m2_success; } -static inline bool m2ValidCoefficient(float x) +M2_NODISCARD static inline bool m2ValidCoefficient(float x) { return m2CheckCoefficient(x) == m2_success; } -static inline bool m2ValidRestitution(float x) +M2_NODISCARD static inline bool m2ValidRestitution(float x) { return m2CheckRestitution(x) == m2_success; } -static inline bool m2ValidDensity(float x) +M2_NODISCARD static inline bool m2ValidDensity(float x) { return m2CheckDensity(x) == m2_success; } -static inline bool m2ValidRatio(float x) +M2_NODISCARD static inline bool m2ValidRatio(float x) { return m2CheckRatio(x) == m2_success; } -static inline bool m2ValidMass(float x) +M2_NODISCARD static inline bool m2ValidMass(float x) { return m2CheckMass(x) == m2_success; } -static inline bool m2ValidInertia(float x) +M2_NODISCARD static inline bool m2ValidInertia(float x) { return m2CheckInertia(x) == m2_success; } -static inline bool m2ValidForce(float x) +M2_NODISCARD static inline bool m2ValidForce(float x) { return m2CheckForce(x) == m2_success; } -static inline bool m2ValidForceVec(m2Vec2 f) +M2_NODISCARD static inline bool m2ValidForceVec(m2Vec2 f) { return m2CheckForceVec(f) == m2_success; } -static inline bool m2ValidMaxForce(float x) +M2_NODISCARD static inline bool m2ValidMaxForce(float x) { return m2CheckMaxForce(x) == m2_success; } -static inline bool m2ValidImpulse(float x) +M2_NODISCARD static inline bool m2ValidSleepThreshold(float x) { - return m2CheckImpulse(x) == m2_success; + return m2CheckSleepThreshold(x) == m2_success; } -static inline bool m2ValidImpulseVec(m2Vec2 j) +M2_NODISCARD static inline bool m2ValidImpulse(float x) { - return m2CheckImpulseVec(j) == m2_success; + return m2CheckImpulse(x) == m2_success; } -static inline bool m2ValidTimeStep(float x) +M2_NODISCARD static inline bool m2ValidImpulseVec(m2Vec2 j) { - return m2CheckTimeStep(x) == m2_success; + return m2CheckImpulseVec(j) == m2_success; } -static inline bool m2ValidAngle(float x) +M2_NODISCARD static inline bool m2ValidAngle(float x) { return m2CheckAngle(x) == m2_success; } -static inline bool m2ValidReach(float x) +M2_NODISCARD static inline bool m2ValidReach(float x) { return m2CheckReach(x) == m2_success; } -static inline bool m2ValidSpan(float x) +M2_NODISCARD static inline bool m2ValidSpan(float x) { return m2CheckSpan(x) == m2_success; } -static inline bool m2ValidFraction(float x) +M2_NODISCARD static inline bool m2ValidFraction(float x) { return m2CheckFraction(x) == m2_success; } -static inline bool m2ValidRatchetTooth(float x) +M2_NODISCARD static inline bool m2ValidRatchetTooth(float x) { return m2CheckRatchetTooth(x) == m2_success; } -static inline bool m2ValidBreakLimit(float x) +M2_NODISCARD static inline bool m2ValidBreakLimit(float x) { return m2CheckBreakLimit(x) == m2_success; } @@ -347,9 +428,18 @@ static inline float m2SaturateF(float x, float limit) return x < -limit ? -limit : (x > limit ? limit : x); } +// A caller box: finite corners, the lower one at or below the upper one +// on both axes. A box of one point is valid; an inverted one contains no +// point, so it is a swapped pair of corners. +M2_NODISCARD static inline bool m2ValidWorldAabb(m2WorldAabb box) +{ + return m2FinitePos2(box.lowerBound) && m2FinitePos2(box.upperBound) && + box.lowerBound.x <= box.upperBound.x && box.lowerBound.y <= box.upperBound.y; +} + // A caller rotation that is stored or used as given must already be a // unit rotation (m2MakeRot builds one); anything else would shear. -static inline bool m2UnitRot(m2Rot q) +M2_NODISCARD static inline bool m2UnitRot(m2Rot q) { float length2 = q.c * q.c + q.s * q.s; return m2FiniteF(q.c) && m2FiniteF(q.s) && length2 > 0.999f && length2 < 1.001f; diff --git a/third_party/maul2d/src/cpu.c b/third_party/maul2d/src/cpu.c index edbf2fcb2..1153b92fa 100644 --- a/third_party/maul2d/src/cpu.c +++ b/third_party/maul2d/src/cpu.c @@ -27,7 +27,7 @@ const char* m2GetSimdBackend(void) #endif } -int32_t m2CpuSupportsBackend(void) +bool m2IsCpuBackendSupported(void) { #if defined(MAUL2D_BUILT_FOR_AVX2) // The AVX2 kernels need AVX2 and FMA3, and the operating system must @@ -38,7 +38,7 @@ int32_t m2CpuSupportsBackend(void) __cpuid(regs, 0); if (regs[0] < 7) { - return 0; // no leaf 7, so no AVX2 + return false; // no leaf 7, so no AVX2 } __cpuidex(regs, 1, 0); int osxsave = (regs[2] >> 27) & 1; @@ -46,29 +46,29 @@ int32_t m2CpuSupportsBackend(void) int avx = (regs[2] >> 28) & 1; if (!(osxsave && avx && fma)) { - return 0; + return false; } unsigned long long xcr = _xgetbv(0); if ((xcr & 0x6) != 0x6) { - return 0; // the OS has not enabled XMM and YMM state + return false; // the OS has not enabled XMM and YMM state } __cpuidex(regs, 7, 0); - return ((regs[1] >> 5) & 1) ? 1 : 0; // leaf 7 EBX bit 5 = AVX2 + return ((regs[1] >> 5) & 1) != 0; // leaf 7 EBX bit 5 = AVX2 #else - return (__builtin_cpu_supports("avx2") && __builtin_cpu_supports("fma")) ? 1 : 0; + return __builtin_cpu_supports("avx2") && __builtin_cpu_supports("fma"); #endif #else // NEON is architectural on arm64, and scalar and wasm run anywhere. - return 1; + return true; #endif } -int32_t m2CheckCpu(const char** backend) +bool m2CheckCpu(const char** backend) { if (backend != NULL) { *backend = m2GetSimdBackend(); } - return m2CpuSupportsBackend(); + return m2IsCpuBackendSupported(); } diff --git a/third_party/maul2d/src/cpu.h b/third_party/maul2d/src/cpu.h index bd7843d91..5d0bb8b17 100644 --- a/third_party/maul2d/src/cpu.h +++ b/third_party/maul2d/src/cpu.h @@ -2,25 +2,30 @@ // Copyright (c) 2026 Sirac Ozmen // // The SIMD backend report and the CPU check (cpu.c), for the library's -// own sources. A library may also declare the first two in its public -// header. +// own sources. A library may also declare any of the three in its +// public header. #ifndef MAUL2D_SRC_CPU_H #define MAUL2D_SRC_CPU_H -#include +#include "maul2d/base.h" + +#include // The backend the library's vector code was built for: "avx2", "neon" // or "scalar". The string is static. +// NOLINTNEXTLINE(readability-redundant-declaration): base.h may declare it const char* m2GetSimdBackend(void); -// 1 when this CPU runs the built backend, else 0. -int32_t m2CpuSupportsBackend(void); +// Whether this CPU runs the built backend. +// NOLINTNEXTLINE(readability-redundant-declaration): base.h may declare it +M2_NODISCARD bool m2IsCpuBackendSupported(void); // The CPU check without a world: stores the backend's name in *backend -// when backend is not null and returns 1 when this CPU runs it, 0 when -// it does not (the build should then be configured scalar). Safe from -// any thread; it keeps no state. -int32_t m2CheckCpu(const char** backend); +// when backend is not null and returns whether this CPU runs it (when it +// does not, the build should be configured scalar). Safe from any thread; +// it keeps no state. +// NOLINTNEXTLINE(readability-redundant-declaration): base.h may declare it +M2_NODISCARD bool m2CheckCpu(const char** backend); #endif diff --git a/third_party/maul2d/src/decompose.c b/third_party/maul2d/src/decompose.c index c0dff9831..592d5f8d0 100644 --- a/third_party/maul2d/src/decompose.c +++ b/third_party/maul2d/src/decompose.c @@ -12,9 +12,10 @@ // --- Convex decomposition ------------------------------------------------------- // -// Ear clipping into triangles, then Hertel-Mehlhorn style merging: -// pieces fuse across a shared edge whenever the union stays strictly -// convex and inside the 8-vertex polygon limit. Every scan runs in +// Ear clipping into triangles, diagonal flips that raise the worst +// triangle's quality, then Hertel-Mehlhorn style merging: pieces fuse +// across a shared edge whenever the union stays strictly convex and +// inside the 8-vertex polygon limit. Every scan runs in // ascending index order and every pass picks the lowest-index // candidate, so the decomposition is canonical: same outline, same // pieces, on every platform. @@ -39,14 +40,14 @@ static double DecompCross(m2Vec2 a, m2Vec2 b, m2Vec2 c) // Inclusive point-in-triangle for a CCW triangle: boundary counts as // inside, which makes the ear test conservative. -static bool DecompPointInTriangle(m2Vec2 a, m2Vec2 b, m2Vec2 c, m2Vec2 p) +M2_NODISCARD static bool DecompPointInTriangle(m2Vec2 a, m2Vec2 b, m2Vec2 c, m2Vec2 p) { return DecompCross(a, b, p) >= 0.0 && DecompCross(b, c, p) >= 0.0 && DecompCross(c, a, p) >= 0.0; } // Whether p, collinear with segment ab, lies on it, endpoints included. -static bool DecompOnSegment(m2Vec2 a, m2Vec2 b, m2Vec2 p) +M2_NODISCARD static bool DecompOnSegment(m2Vec2 a, m2Vec2 b, m2Vec2 p) { return (a.x <= b.x ? a.x <= p.x && p.x <= b.x : b.x <= p.x && p.x <= a.x) && (a.y <= b.y ? a.y <= p.y && p.y <= b.y : b.y <= p.y && p.y <= a.y); @@ -56,7 +57,7 @@ static bool DecompOnSegment(m2Vec2 a, m2Vec2 b, m2Vec2 p) // the other (a vertex on an edge, a repeated vertex, a collinear // overlap). An outline whose edges meet anywhere but at their shared // corners is not simple, and its pieces could cover what it cuts out. -static bool DecompSegmentsMeet(m2Vec2 a, m2Vec2 b, m2Vec2 c, m2Vec2 d) +M2_NODISCARD static bool DecompSegmentsMeet(m2Vec2 a, m2Vec2 b, m2Vec2 c, m2Vec2 d) { double d1 = DecompCross(c, d, a); double d2 = DecompCross(c, d, b); @@ -75,7 +76,7 @@ static bool DecompSegmentsMeet(m2Vec2 a, m2Vec2 b, m2Vec2 c, m2Vec2 d) // it arrived on: a spike of zero width. The edge test alone would catch a // fold through a different cross product when the fold overshoots, and // rounding can tell the two apart, so the corner is tested exactly here. -static bool DecompFoldsBack(m2Vec2 a, m2Vec2 b, m2Vec2 c) +M2_NODISCARD static bool DecompFoldsBack(m2Vec2 a, m2Vec2 b, m2Vec2 c) { double dot = ((double)b.x - (double)a.x) * ((double)c.x - (double)b.x) + ((double)b.y - (double)a.y) * ((double)c.y - (double)b.y); @@ -85,7 +86,7 @@ static bool DecompFoldsBack(m2Vec2 a, m2Vec2 b, m2Vec2 c) // Whether p lies within M2_LINEAR_SLOP of segment ab. Closer than that, // a vertex touches the edge for the solver, and float rounding in the // pieces can make them cross it. -static bool DecompNear(m2Vec2 a, m2Vec2 b, m2Vec2 p) +M2_NODISCARD static bool DecompNear(m2Vec2 a, m2Vec2 b, m2Vec2 p) { double ex = (double)b.x - (double)a.x; double ey = (double)b.y - (double)a.y; @@ -106,7 +107,7 @@ static bool DecompNear(m2Vec2 a, m2Vec2 b, m2Vec2 p) // Whether an outline is simple: no corner folds back, no two edges meet // except adjacent ones at their shared corner, and no corner comes within // M2_LINEAR_SLOP of an edge it does not end. -static bool DecompSimple(const m2Vec2* points, int32_t count) +M2_NODISCARD static bool DecompSimple(const m2Vec2* points, int32_t count) { for (int32_t i = 0; i < count; ++i) { @@ -139,6 +140,133 @@ static bool DecompSimple(const m2Vec2* points, int32_t count) return true; } +// Whether a piece passes the polygon checks every shape does. The +// convexity test of the merge works in double, the checks in float: a +// corner that turns by a hair in double can be straight or reflex in +// float. +M2_NODISCARD static bool DecompPieceValid(const m2Vec2* points, const m2DecompPiece* piece) +{ + m2Vec2 verts[M2_MAX_POLYGON_VERTICES]; + for (int32_t k = 0; k < piece->n; ++k) + { + verts[k] = points[piece->idx[k]]; + } + return m2BuildPolygon(verts, piece->n, 0.0f).count != 0; +} + +// A triangle's quality: twice its area over its longest edge squared, +// near 0 for a sliver. Taken from its lowest index, so the same three +// corners always give the same bits, whatever order they come in. +static double DecompQuality(const m2Vec2* points, int32_t a, int32_t b, int32_t c) +{ + while (!(a < b && a < c)) + { + int32_t first = a; + a = b; + b = c; + c = first; + } + m2Vec2 pa = points[a]; + m2Vec2 pb = points[b]; + m2Vec2 pc = points[c]; + double abx = (double)pb.x - (double)pa.x; + double aby = (double)pb.y - (double)pa.y; + double bcx = (double)pc.x - (double)pb.x; + double bcy = (double)pc.y - (double)pb.y; + double cax = (double)pa.x - (double)pc.x; + double cay = (double)pa.y - (double)pc.y; + double ab = abx * abx + aby * aby; + double bc = bcx * bcx + bcy * bcy; + double ca = cax * cax + cay * cay; + double longest = ab > bc ? (ab > ca ? ab : ca) : (bc > ca ? bc : ca); + return DecompCross(pa, pb, pc) / longest; +} + +// The corner of a triangle opposite its edge from v to u, or -1 when the +// triangle has no such edge. +static int32_t DecompOpposite(const int32_t* triangle, int32_t u, int32_t v) +{ + for (int32_t g = 0; g < 3; ++g) + { + if (triangle[g] == v && triangle[(g + 1) % 3] == u) + { + return triangle[(g + 2) % 3]; + } + } + return -1; +} + +// Flips the diagonal triangle s (u, v, w) shares with triangle o +// (v, u, x) to the quad's other diagonal, when the quad is strictly +// convex and the worse of the two new triangles is better than the worse +// of the two old ones. Returns whether it flipped. +M2_NODISCARD static bool DecompFlip(const m2Vec2* points, int32_t* s, int32_t* o, int32_t e) +{ + int32_t u = s[e]; + int32_t v = s[(e + 1) % 3]; + int32_t w = s[(e + 2) % 3]; + int32_t x = DecompOpposite(o, u, v); + if (DecompCross(points[w], points[u], points[x]) <= 0.0 || + DecompCross(points[x], points[v], points[w]) <= 0.0) + { + return false; + } + double oldFirst = DecompQuality(points, u, v, w); + double oldSecond = DecompQuality(points, v, u, x); + double newFirst = DecompQuality(points, w, u, x); + double newSecond = DecompQuality(points, x, v, w); + double before = oldFirst < oldSecond ? oldFirst : oldSecond; + double after = newFirst < newSecond ? newFirst : newSecond; + if (!(after > before)) + { + return false; + } + s[0] = w; + s[1] = u; + s[2] = x; + o[0] = x; + o[1] = v; + o[2] = w; + return true; +} + +// Raises the triangulation's worst triangles (Lawson's local +// optimization, with the quality above for the angle), so a long, thin +// triangle the ears cut usually becomes two that fuse into pieces +// validation keeps. Nothing guarantees it: a sliver with no strictly +// convex quad around an inner edge, or whose flip does not raise the +// worse quality, stays, and the merge and validation below treat it as +// before. Each pass walks triangles and their edges in ascending order and +// flips every inner diagonal that qualifies. It ends: a quality depends +// only on its three corners, and a flip replaces two triangles whose +// worse quality is q by two whose qualities both exceed q, so the +// triangulation's qualities, sorted, rise in lexicographic order with +// every flip, and an outline has finitely many triangulations. +static void DecompRaiseWorst(const m2Vec2* points, int32_t* triangles, int32_t triangleCount) +{ + bool flipped = true; + while (flipped) + { + flipped = false; + for (int32_t s = 0; s < triangleCount; ++s) + { + for (int32_t e = 0; e < 3; ++e) + { + int32_t* first = triangles + 3 * s; + for (int32_t o = s + 1; o < triangleCount; ++o) + { + int32_t* second = triangles + 3 * o; + if (DecompOpposite(second, first[e], first[(e + 1) % 3]) >= 0) + { + flipped = DecompFlip(points, first, second, e) || flipped; + break; + } + } + } + } + } +} + // The decomposition behind m2DecomposeOutline: the true piece total, or // -1 for an outline it refuses. // NOLINTNEXTLINE(readability-function-cognitive-complexity): one ring: clip ears, merge, validate @@ -243,10 +371,13 @@ static int32_t Decompose(const m2Vec2* points, int32_t count, m2Polygon* pieces, triangles[3 * triangleCount + 2] = ring[2]; triangleCount += 1; } + DecompRaiseWorst(points, triangles, triangleCount); // Merge pass: fuse two pieces across a shared edge whenever the - // union stays strictly convex and at most 8 vertices. Ascending - // pair order, restart after every fuse: canonical. + // union stays strictly convex, at most 8 vertices and passes the + // polygon checks, so a fuse never turns two kept pieces into one the + // validation below drops. Ascending pair order, restart after every + // fuse: canonical. m2DecompPiece work[M2_MAX_OUTLINE_POINTS - 2]; int32_t pieceCount = triangleCount; for (int32_t t = 0; t < triangleCount; ++t) @@ -304,7 +435,7 @@ static int32_t Decompose(const m2Vec2* points, int32_t count, m2Polygon* pieces, m2Vec2 c = points[merged.idx[(k + 2) % merged.n]]; convex = DecompCross(a, b, c) > 0.0; } - if (!convex) + if (!convex || !DecompPieceValid(points, &merged)) { continue; } @@ -365,7 +496,7 @@ static M2_NOINLINE m2Result DecomposeOutlineWorker(const m2Vec2* points, int32_t m2Polygon* pieces, int32_t capacity, int32_t* countOut) { - if (countOut == NULL) + if (countOut == NULL || capacity < 0 || (pieces == NULL && capacity > 0)) { return m2_errorInvalid; } @@ -383,11 +514,11 @@ static M2_NOINLINE m2Result DecomposeOutlineWorker(const m2Vec2* points, int32_t return m2_success; } -m2Result m2DecomposeOutline(const m2Vec2* points, int32_t count, m2Polygon* pieces, +m2Result m2DecomposeOutline(const m2Vec2* points, int32_t count, m2Polygon* piecesOut, int32_t capacity, int32_t* countOut) { uint64_t env = m2FloatEnvEnter(); - m2Result result = DecomposeOutlineWorker(points, count, pieces, capacity, countOut); + m2Result result = DecomposeOutlineWorker(points, count, piecesOut, capacity, countOut); m2FloatEnvLeave(env); return result; } diff --git a/third_party/maul2d/src/destruction.c b/third_party/maul2d/src/destruction.c index bb30e62dc..2168d09b5 100644 --- a/third_party/maul2d/src/destruction.c +++ b/third_party/maul2d/src/destruction.c @@ -198,11 +198,25 @@ static m2BodyId CreatePiece(m2World* world, int32_t parent, const m2ShapeDef* sh return piece; } -static M2_NOINLINE m2Result WorldShatterBodyWorker(m2World* world, m2BodyId bodyId, - const m2Polygon* pieces, int32_t pieceCount, - m2BodyId* bodiesOut, int32_t capacity, - int32_t* countOut) +static M2_NOINLINE m2Result BodyShatterWorker(m2World* world, m2BodyId bodyId, + const m2Polygon* pieces, int32_t pieceCount, + m2BodyId* bodiesOut, int32_t capacity, + int32_t* countOut) { + // bodiesOut holds the ids of bodies the call makes, so every failure + // leaves null ids there, a busy refusal too: no failed call leaves an id + // the caller could destroy. A busy refusal leaves the count, which host + // code the world is running may be filling; every other failure clears + // it. + m2Result writer = m2CheckWriter(world); + for (int32_t i = 0; bodiesOut != NULL && i < capacity; ++i) + { + bodiesOut[i] = m2_nullBodyId; + } + if (writer == m2_errorBusy) + { + return writer; + } if (countOut != NULL) { *countOut = 0; @@ -211,11 +225,6 @@ static M2_NOINLINE m2Result WorldShatterBodyWorker(m2World* world, m2BodyId body { return m2Refuse(world, m2_errorInvalid); } - for (int32_t i = 0; i < capacity; ++i) - { - bodiesOut[i] = m2_nullBodyId; - } - m2Result writer = m2CheckWriter(world); if (writer != m2_success) { return writer; @@ -263,13 +272,13 @@ static M2_NOINLINE m2Result WorldShatterBodyWorker(m2World* world, m2BodyId body return m2_success; } -m2Result m2World_ShatterBody(m2World* world, m2BodyId bodyId, const m2Polygon* pieces, - int32_t pieceCount, m2BodyId* bodiesOut, int32_t capacity, - int32_t* countOut) +m2Result m2Body_Shatter(m2World* world, m2BodyId bodyId, const m2Polygon* pieces, + int32_t pieceCount, m2BodyId* bodiesOut, int32_t capacity, + int32_t* countOut) { uint64_t env = m2FloatEnvEnter(); m2Result result = - WorldShatterBodyWorker(world, bodyId, pieces, pieceCount, bodiesOut, capacity, countOut); + BodyShatterWorker(world, bodyId, pieces, pieceCount, bodiesOut, capacity, countOut); m2FloatEnvLeave(env); return result; } diff --git a/third_party/maul2d/src/distance.c b/third_party/maul2d/src/distance.c index 6c05e2244..9f79bab71 100644 --- a/third_party/maul2d/src/distance.c +++ b/third_party/maul2d/src/distance.c @@ -151,7 +151,7 @@ static m2Vec2 Blend(const Simplex* s, bool pointsOfA) return p; } -static bool Repeats(const Simplex* s, int32_t ia, int32_t ib) +M2_NODISCARD static bool Repeats(const Simplex* s, int32_t ia, int32_t ib) { for (int32_t i = 0; i < s->count; ++i) { diff --git a/third_party/maul2d/src/dynamic_tree.c b/third_party/maul2d/src/dynamic_tree.c index 7900b75e2..7c66d06bd 100644 --- a/third_party/maul2d/src/dynamic_tree.c +++ b/third_party/maul2d/src/dynamic_tree.c @@ -334,44 +334,6 @@ bool m2TreeNextQuery(m2TreeCursor* cursor, int32_t* userData) return false; } -// Checks one subtree and returns its leaf count, or -1 on any breach: -// links both ways, the height rule, AVL balance and box containment. -static int32_t CheckSubtree(const m2DynamicTree* tree, const m2TreeNode* nodes, int32_t node) -{ - const m2TreeNode* n = nodes + node; - if (n->height == 0) - { - return n->child1 == M2_NULL_NODE && n->child2 == M2_NULL_NODE ? 1 : -1; - } - int32_t a = n->child1; - int32_t b = n->child2; - if (a < 0 || a >= tree->nodeCapacity || b < 0 || b >= tree->nodeCapacity || - nodes[a].parent != node || nodes[b].parent != node || - n->height != 1 + MaxHeight(nodes, a, b) || nodes[a].height - nodes[b].height > 1 || - nodes[b].height - nodes[a].height > 1 || !m2Aabb_Contains(n->aabb, nodes[a].aabb) || - !m2Aabb_Contains(n->aabb, nodes[b].aabb)) - { - return -1; - } - int32_t leavesA = CheckSubtree(tree, nodes, a); - int32_t leavesB = CheckSubtree(tree, nodes, b); - return leavesA < 0 || leavesB < 0 ? -1 : leavesA + leavesB; -} - -bool m2TreeValidate(const m2DynamicTree* tree, const m2TreeNode* nodes) -{ - if (tree->root == M2_NULL_NODE) - { - return tree->nodeCount == 0; - } - if (nodes[tree->root].parent != M2_NULL_NODE) - { - return false; - } - int32_t leaves = CheckSubtree(tree, nodes, tree->root); - return leaves > 0 && tree->nodeCount == 2 * leaves - 1; -} - void m2TreeKeepLowest(int32_t* kept, int32_t* count, int32_t capacity, int32_t value) { int32_t n = *count; diff --git a/third_party/maul2d/src/dynamic_tree.h b/third_party/maul2d/src/dynamic_tree.h index c9ec32531..f2dee8d51 100644 --- a/third_party/maul2d/src/dynamic_tree.h +++ b/third_party/maul2d/src/dynamic_tree.h @@ -78,24 +78,20 @@ void m2TreeBeginQuery(m2TreeCursor* cursor, const m2DynamicTree* tree, const m2T m2Aabb aabb); // Writes the next overlapping leaf's userData and returns true, or // returns false when the query is exhausted. -bool m2TreeNextQuery(m2TreeCursor* cursor, int32_t* userData); +M2_NODISCARD bool m2TreeNextQuery(m2TreeCursor* cursor, int32_t* userData); // Offers value to kept, the lowest values offered so far in ascending // order, at most capacity of them (*count): a full set drops its largest // for a lower value and ignores a higher one. void m2TreeKeepLowest(int32_t* kept, int32_t* count, int32_t capacity, int32_t value); -// Test oracle: links both ways, heights, AVL balance, box containment -// and the node count. Returns false on any breach. -bool m2TreeValidate(const m2DynamicTree* tree, const m2TreeNode* nodes); - -static inline bool m2Aabb_Overlaps(m2Aabb a, m2Aabb b) +M2_NODISCARD static inline bool m2Aabb_Overlaps(m2Aabb a, m2Aabb b) { return a.lowerBound.x <= b.upperBound.x && a.lowerBound.y <= b.upperBound.y && b.lowerBound.x <= a.upperBound.x && b.lowerBound.y <= a.upperBound.y; } -static inline bool m2Aabb_Contains(m2Aabb outer, m2Aabb inner) +M2_NODISCARD static inline bool m2Aabb_Contains(m2Aabb outer, m2Aabb inner) { return outer.lowerBound.x <= inner.lowerBound.x && outer.lowerBound.y <= inner.lowerBound.y && inner.upperBound.x <= outer.upperBound.x && inner.upperBound.y <= outer.upperBound.y; diff --git a/third_party/maul2d/src/format_layout.c b/third_party/maul2d/src/format_layout.c index f516ee80c..f79c57c8b 100644 --- a/third_party/maul2d/src/format_layout.c +++ b/third_party/maul2d/src/format_layout.c @@ -69,9 +69,9 @@ M2_PIN_SIZE(m2ParticleId, 8); M2_PIN_AT(m2ParticleId, index1, 0); M2_PIN_AT(m2ParticleId, generation, 4); -M2_PIN_SIZE(m2FluidVolumeId, 8); -M2_PIN_AT(m2FluidVolumeId, index1, 0); -M2_PIN_AT(m2FluidVolumeId, generation, 4); +M2_PIN_SIZE(m2WaterVolumeId, 8); +M2_PIN_AT(m2WaterVolumeId, index1, 0); +M2_PIN_AT(m2WaterVolumeId, generation, 4); M2_PIN_SIZE(m2Circle, 12); M2_PIN_AT(m2Circle, center, 0); @@ -141,6 +141,17 @@ M2_PIN_AT(m2DynamicTree, nodeCount, 4); M2_PIN_AT(m2DynamicTree, nodeCapacity, 8); M2_PIN_AT(m2DynamicTree, freeList, 12); +// The end events queued between steps. +M2_PIN_SIZE(m2ContactEndEvent, 24); +M2_PIN_AT(m2ContactEndEvent, shapeIdA, 0); +M2_PIN_AT(m2ContactEndEvent, shapeIdB, 8); +M2_PIN_AT(m2ContactEndEvent, step, 16); + +M2_PIN_SIZE(m2SensorEndEvent, 24); +M2_PIN_AT(m2SensorEndEvent, sensorShapeId, 0); +M2_PIN_AT(m2SensorEndEvent, visitorShapeId, 8); +M2_PIN_AT(m2SensorEndEvent, step, 16); + // The public defs a record embeds as given. M2_PIN_SIZE(m2BodyDef, 80); M2_PIN_AT(m2BodyDef, type, 0); @@ -153,11 +164,12 @@ M2_PIN_AT(m2BodyDef, linearDamping, 48); M2_PIN_AT(m2BodyDef, angularDamping, 52); M2_PIN_AT(m2BodyDef, motionLocks, 56); M2_PIN_AT(m2BodyDef, enableSleep, 59); -M2_PIN_AT(m2BodyDef, isEnabled, 60); +M2_PIN_AT(m2BodyDef, enable, 60); M2_PIN_AT(m2BodyDef, dominance, 61); -M2_PIN_AT(m2BodyDef, isBullet, 62); +M2_PIN_AT(m2BodyDef, enableBullet, 62); M2_PIN_AT(m2BodyDef, userData, 64); M2_PIN_AT(m2BodyDef, internalValue, 72); +M2_PIN_AT(m2BodyDef, sleepThreshold, 76); M2_PIN_SIZE(m2MassData, 16); M2_PIN_AT(m2MassData, mass, 0); @@ -176,16 +188,15 @@ M2_PIN_AT(m2ShapeDef, isSensor, 36); M2_PIN_AT(m2ShapeDef, userData, 40); M2_PIN_AT(m2ShapeDef, internalValue, 48); -M2_PIN_SIZE(m2FluidVolumeDef, 80); -M2_PIN_AT(m2FluidVolumeDef, regionLower, 0); -M2_PIN_AT(m2FluidVolumeDef, regionUpper, 16); -M2_PIN_AT(m2FluidVolumeDef, surface, 32); -M2_PIN_AT(m2FluidVolumeDef, density, 40); -M2_PIN_AT(m2FluidVolumeDef, linearDrag, 44); -M2_PIN_AT(m2FluidVolumeDef, angularDrag, 48); -M2_PIN_AT(m2FluidVolumeDef, flow, 52); -M2_PIN_AT(m2FluidVolumeDef, userData, 64); -M2_PIN_AT(m2FluidVolumeDef, internalValue, 72); +M2_PIN_SIZE(m2WaterVolumeDef, 80); +M2_PIN_AT(m2WaterVolumeDef, bounds, 0); +M2_PIN_AT(m2WaterVolumeDef, surface, 32); +M2_PIN_AT(m2WaterVolumeDef, density, 40); +M2_PIN_AT(m2WaterVolumeDef, linearDrag, 44); +M2_PIN_AT(m2WaterVolumeDef, angularDrag, 48); +M2_PIN_AT(m2WaterVolumeDef, flow, 52); +M2_PIN_AT(m2WaterVolumeDef, userData, 64); +M2_PIN_AT(m2WaterVolumeDef, internalValue, 72); M2_PIN_SIZE(m2DistanceJointDef, 72); M2_PIN_AT(m2DistanceJointDef, bodyIdA, 0); @@ -214,7 +225,7 @@ M2_PIN_AT(m2RevoluteJointDef, springDampingRatio, 44); M2_PIN_AT(m2RevoluteJointDef, enableSpring, 48); M2_PIN_AT(m2RevoluteJointDef, enableMotor, 49); M2_PIN_AT(m2RevoluteJointDef, motorSpeed, 52); -M2_PIN_AT(m2RevoluteJointDef, maxMotorTorque, 56); +M2_PIN_AT(m2RevoluteJointDef, maxMotorEffort, 56); M2_PIN_AT(m2RevoluteJointDef, enableLimit, 60); M2_PIN_AT(m2RevoluteJointDef, lowerAngle, 64); M2_PIN_AT(m2RevoluteJointDef, upperAngle, 68); @@ -232,7 +243,7 @@ M2_PIN_AT(m2PrismaticJointDef, constraintHertz, 40); M2_PIN_AT(m2PrismaticJointDef, constraintDampingRatio, 44); M2_PIN_AT(m2PrismaticJointDef, enableMotor, 48); M2_PIN_AT(m2PrismaticJointDef, motorSpeed, 52); -M2_PIN_AT(m2PrismaticJointDef, maxMotorForce, 56); +M2_PIN_AT(m2PrismaticJointDef, maxMotorEffort, 56); M2_PIN_AT(m2PrismaticJointDef, enableLimit, 60); M2_PIN_AT(m2PrismaticJointDef, lowerTranslation, 64); M2_PIN_AT(m2PrismaticJointDef, upperTranslation, 68); @@ -264,7 +275,7 @@ M2_PIN_AT(m2WheelJointDef, springHertz, 44); M2_PIN_AT(m2WheelJointDef, springDampingRatio, 48); M2_PIN_AT(m2WheelJointDef, enableMotor, 52); M2_PIN_AT(m2WheelJointDef, motorSpeed, 56); -M2_PIN_AT(m2WheelJointDef, maxMotorTorque, 60); +M2_PIN_AT(m2WheelJointDef, maxMotorEffort, 60); M2_PIN_AT(m2WheelJointDef, enableLimit, 64); M2_PIN_AT(m2WheelJointDef, lowerTranslation, 68); M2_PIN_AT(m2WheelJointDef, upperTranslation, 72); @@ -326,7 +337,7 @@ M2_PIN_AT(m2PulleyJointDef, internalValue, 84); M2_PIN_SIZE(m2RatchetJointDef, 40); M2_PIN_AT(m2RatchetJointDef, bodyIdA, 0); M2_PIN_AT(m2RatchetJointDef, bodyIdB, 8); -M2_PIN_AT(m2RatchetJointDef, ratchet, 16); +M2_PIN_AT(m2RatchetJointDef, toothAngle, 16); M2_PIN_AT(m2RatchetJointDef, phase, 20); M2_PIN_AT(m2RatchetJointDef, userData, 24); M2_PIN_AT(m2RatchetJointDef, enableConnectedCollision, 32); @@ -464,13 +475,17 @@ M2_PIN_SIZE(m2OpParticleUserData, 16); M2_PIN_AT(m2OpParticleUserData, id, 0); M2_PIN_AT(m2OpParticleUserData, userData, 8); -M2_PIN_SIZE(m2OpCreateFluidVolume, 88); -M2_PIN_AT(m2OpCreateFluidVolume, def, 0); -M2_PIN_AT(m2OpCreateFluidVolume, expected, 80); +M2_PIN_SIZE(m2OpCreateWaterVolume, 88); +M2_PIN_AT(m2OpCreateWaterVolume, def, 0); +M2_PIN_AT(m2OpCreateWaterVolume, expected, 80); + +M2_PIN_SIZE(m2OpWaterSurface, 16); +M2_PIN_AT(m2OpWaterSurface, id, 0); +M2_PIN_AT(m2OpWaterSurface, surface, 8); -M2_PIN_SIZE(m2OpFluidSurface, 16); -M2_PIN_AT(m2OpFluidSurface, id, 0); -M2_PIN_AT(m2OpFluidSurface, surface, 8); +M2_PIN_SIZE(m2OpWaterUserData, 16); +M2_PIN_AT(m2OpWaterUserData, id, 0); +M2_PIN_AT(m2OpWaterUserData, userData, 8); M2_PIN_SIZE(m2OpShatterHeader, 16); M2_PIN_AT(m2OpShatterHeader, body, 0); diff --git a/third_party/maul2d/src/geometry.c b/third_party/maul2d/src/geometry.c index 7015e1c89..e9b60d643 100644 --- a/third_party/maul2d/src/geometry.c +++ b/third_party/maul2d/src/geometry.c @@ -222,30 +222,59 @@ static int32_t MonotoneChain(m2Vec2* ps, int32_t n, m2Vec2* hull) return k - 1; // the last point repeats the first } +// Whether every chain point from after a to before b, cyclically, lies +// within two linear slops of the line through chain[a] and chain[b]. +M2_NODISCARD static bool SpanIsFlat(const m2Vec2* chain, int32_t n, int32_t a, int32_t b) +{ + for (int32_t j = (a + 1) % n; j != b; j = (j + 1) % n) + { + if (LineDistance(chain[a], chain[b], chain[j]) > 2.0f * M2_LINEAR_SLOP) + { + return false; + } + } + return true; +} + // Drops vertices within two linear slops of the line through their -// neighbors, one at a time, lowest index first, until none is left. +// neighbors, one at a time, lowest index first, until none is left. The +// points dropped earlier between those neighbors must stay within two +// slops of the line too, so the hull never moves further than that from +// any point of the chain. The chain holds as many points as BuildHull's +// hull buffer: in float, a point near collinear may sit on both of the +// monotone chain's halves. static int32_t MergeCollinear(m2Vec2* hull, int32_t n) { + m2Vec2 chain[2 * M2_MAX_HULL_POINTS]; + int32_t kept[2 * M2_MAX_HULL_POINTS]; + for (int32_t i = 0; i < n; ++i) + { + chain[i] = hull[i]; + kept[i] = i; + } + int32_t m = n; bool merged = true; - while (merged && n > 2) + while (merged && m > 2) { merged = false; - for (int32_t i = 0; i < n && !merged; ++i) + for (int32_t i = 0; i < m && !merged; ++i) { - m2Vec2 prev = hull[(i + n - 1) % n]; - m2Vec2 next = hull[(i + 1) % n]; - if (LineDistance(prev, next, hull[i]) <= 2.0f * M2_LINEAR_SLOP) + if (SpanIsFlat(chain, n, kept[(i + m - 1) % m], kept[(i + 1) % m])) { - for (int32_t j = i; j < n - 1; ++j) + for (int32_t j = i; j < m - 1; ++j) { - hull[j] = hull[j + 1]; + kept[j] = kept[j + 1]; } - n -= 1; + m -= 1; merged = true; } } } - return n; + for (int32_t i = 0; i < m; ++i) + { + hull[i] = chain[kept[i]]; + } + return m; } // The bounds of caller points and a radius: a point or the radius past @@ -679,43 +708,3 @@ m2Polygon m2MakeSegmentProxy(m2Vec2 p1, m2Vec2 p2, float radius) proxy.radius = radius; return proxy; } - -float m2ShapeArea(const m2ShapeGeometry* g) -{ - switch (g->type) - { - case m2_circleShape: - return M2_PI * g->circle.radius * g->circle.radius; - case m2_capsuleShape: - { - m2Vec2 p1 = g->capsule.point1; - m2Vec2 p2 = g->capsule.point2; - float r = g->capsule.radius; - float dx = p2.x - p1.x; - float dy = p2.y - p1.y; - float length = sqrtf(dx * dx + dy * dy); - return 2.0f * r * length + M2_PI * r * r; - } - case m2_polygonShape: - { - float area2 = 0.0f; - for (int32_t i = 0; i < g->polygon.count; ++i) - { - m2Vec2 a = g->polygon.vertices[i]; - m2Vec2 b = g->polygon.vertices[(i + 1) % g->polygon.count]; - area2 += a.x * b.y - b.x * a.y; - } - return 0.5f * (area2 < 0.0f ? -area2 : area2); - } - case m2_segmentShape: - case m2_chainSegmentShape: - { - const m2Segment* seg = g->type == m2_segmentShape ? &g->segment : &g->chainSegment.segment; - float dx = seg->point2.x - seg->point1.x; - float dy = seg->point2.y - seg->point1.y; - return 0.05f * sqrtf(dx * dx + dy * dy); // a thin sliver, nominal - } - default: - return 0.0f; - } -} diff --git a/third_party/maul2d/src/geometry.h b/third_party/maul2d/src/geometry.h index 00e592e08..e3a69a8ab 100644 --- a/third_party/maul2d/src/geometry.h +++ b/third_party/maul2d/src/geometry.h @@ -31,11 +31,11 @@ typedef struct m2ShapeGeometry _Static_assert(sizeof(m2Polygon) == 136, "polygon layout is ABI"); _Static_assert(sizeof(m2ShapeGeometry) == 140, "geometry union must be padding-free"); -bool m2ValidateCircle(const m2Circle* circle); -bool m2ValidateCapsule(const m2Capsule* capsule); -bool m2ValidateSegment(const m2Segment* segment); +M2_NODISCARD bool m2ValidateCircle(const m2Circle* circle); +M2_NODISCARD bool m2ValidateCapsule(const m2Capsule* capsule); +M2_NODISCARD bool m2ValidateSegment(const m2Segment* segment); // m2ValidatePolygon is internal; the validator family is not public API. -bool m2ValidatePolygon(const m2Polygon* polygon); +M2_NODISCARD bool m2ValidatePolygon(const m2Polygon* polygon); // The same checks with the status a call returns: coordinates or a radius // past M2_LOCAL_LIMIT, or a polygon past M2_MAX_POLYGON_VERTICES, are @@ -65,12 +65,12 @@ m2Polygon m2CanonicalPolygon(const m2Polygon* polygon); // Rebuilds a polygon geometry from its vertices; other shapes stay as // they are. False when the vertices do not make a polygon. -bool m2CanonicalizeGeometry(m2ShapeGeometry* geometry); +M2_NODISCARD bool m2CanonicalizeGeometry(m2ShapeGeometry* geometry); // Whether a geometry is one the library would keep: each shape passes its // creation check, a chain segment's ghosts are finite, and a polygon is // exactly its canonical form. Restore asks this of every live shape. -bool m2GeometryIsCanonical(const m2ShapeGeometry* geometry); +M2_NODISCARD bool m2GeometryIsCanonical(const m2ShapeGeometry* geometry); m2Aabb m2ComputeShapeAabb(const m2ShapeGeometry* geometry, m2Transform xf); m2MassData m2ComputeShapeMass(const m2ShapeGeometry* geometry, float density); @@ -134,7 +134,4 @@ m2Polygon m2MakeSegmentProxy(m2Vec2 p1, m2Vec2 p2, float radius); // kernel for bullet CCD. float m2PointShapeDistance(const m2ShapeGeometry* geometry, m2Vec2 point); -// The area of a shape geometry (zero for segments). -float m2ShapeArea(const struct m2ShapeGeometry* g); - #endif // MAUL2D_SRC_GEOMETRY_H diff --git a/third_party/maul2d/src/hash.c b/third_party/maul2d/src/hash.c index 59a023eff..d836f1ef9 100644 --- a/third_party/maul2d/src/hash.c +++ b/third_party/maul2d/src/hash.c @@ -8,7 +8,7 @@ #include -uint64_t m2Hash64(uint64_t seed, const void* data, int32_t byteCount) +uint64_t m2Hash64(uint64_t seed, const void* data, int64_t byteCount) { // Eight bytes a round: each word, read in the host's byte order like // every value the hash covers, is xored into the state, the state is @@ -18,7 +18,7 @@ uint64_t m2Hash64(uint64_t seed, const void* data, int32_t byteCount) // are frozen: the hash feeds the determinism gates. const uint8_t* bytes = (const uint8_t*)data; uint64_t hash = seed; - int32_t i = 0; + int64_t i = 0; for (; i + 8 <= byteCount; i += 8) { uint64_t word; diff --git a/third_party/maul2d/src/island.c b/third_party/maul2d/src/island.c index d6f2ee56a..97bfda8a2 100644 --- a/third_party/maul2d/src/island.c +++ b/third_party/maul2d/src/island.c @@ -19,9 +19,7 @@ #include "maul2d/base.h" -#define M2_SLEEP_LINEAR_TOLERANCE 0.05f // m/s -#define M2_SLEEP_ANGULAR_TOLERANCE 0.12f // rad/s -#define M2_TIME_TO_SLEEP 0.5f // seconds under tolerance +#define M2_TIME_TO_SLEEP 0.5f // seconds under tolerance static int32_t Find(int32_t* parent, int32_t i) { @@ -48,12 +46,19 @@ static void Union(int32_t* parent, int32_t a, int32_t b) } } -static bool BodySlow(const m2World* world, int32_t i) +// A body rests below its sleep threshold t and below a spin that scales +// with it. The spin limit is the angular tolerance times t over the +// default on purpose: at the default the quotient is exactly 1.0f, so the +// limit is exactly M2_SLEEP_ANGULAR_TOLERANCE and t * t is the default's +// own square, the bits of a compare against the constants. A form such as +// t * 2.4f rounds to another float at the default and moves results. +M2_NODISCARD static bool BodySlow(const m2World* world, int32_t i) { m2Vec2 v = world->bodies.linearVelocities[i]; float w = world->bodies.angularVelocities[i]; - return v.x * v.x + v.y * v.y < M2_SLEEP_LINEAR_TOLERANCE * M2_SLEEP_LINEAR_TOLERANCE && - w > -M2_SLEEP_ANGULAR_TOLERANCE && w < M2_SLEEP_ANGULAR_TOLERANCE; + float t = world->bodies.sleepThresholds[i]; + float spin = M2_SLEEP_ANGULAR_TOLERANCE * (t / M2_SLEEP_LINEAR_TOLERANCE); + return v.x * v.x + v.y * v.y < t * t && w > -spin && w < spin; } // Pre-solve: build islands over touching contacts, wake islands that @@ -158,7 +163,7 @@ void m2UpdateIslandsAndWake(m2World* world) } // The bodies the sleep pass accounts: alive, dynamic and awake. -static bool AwakeDynamic(const m2World* world, int32_t i) +M2_NODISCARD static bool AwakeDynamic(const m2World* world, int32_t i) { return world->bodies.alive[i] != 0 && world->bodies.types[i] == (uint8_t)m2_dynamicBody && world->bodies.asleep[i] == 0; diff --git a/third_party/maul2d/src/island.h b/third_party/maul2d/src/island.h index 6cedb8e57..8454122fb 100644 --- a/third_party/maul2d/src/island.h +++ b/third_party/maul2d/src/island.h @@ -8,6 +8,15 @@ #include "world_internal.h" +#define M2_SLEEP_LINEAR_TOLERANCE 0.05f // m/s: the default sleep threshold +#define M2_SLEEP_ANGULAR_TOLERANCE 0.12f // rad/s at the default threshold + +// The threshold a body keeps for one a caller gives: 0 is the default. +static inline float m2ResolveSleepThreshold(float threshold) +{ + return threshold > 0.0f ? threshold : M2_SLEEP_LINEAR_TOLERANCE; +} + // Wakes whole islands touched by an awake body, then puts islands to // sleep once every body in them has rested long enough. void m2UpdateIslandsAndWake(m2World* world); diff --git a/third_party/maul2d/src/joint.c b/third_party/maul2d/src/joint.c index 66fbfd2fb..66117d355 100644 --- a/third_party/maul2d/src/joint.c +++ b/third_party/maul2d/src/joint.c @@ -255,7 +255,7 @@ int32_t m2JointTuningSlot(const m2World* world, m2JointId jointId, m2JointTuning // The contract of each joint parameter channel: the joint kinds that // carry the parameter and the values it takes. Live setters and replay // both pass through it, so a tape can never write what the API refuses. -static bool JointParamValid(uint8_t type, uint8_t param, float value) +M2_NODISCARD static bool JointParamValid(uint8_t type, uint8_t param, float value) { bool motorAndLimits = (M2_MOTOR_LIMIT_KINDS & (1u << type)) != 0; switch (param) @@ -272,6 +272,8 @@ static bool JointParamValid(uint8_t type, uint8_t param, float value) (type == (uint8_t)m2_revoluteJoint ? m2ValidAngle(value) : m2ValidReach(value)); case m2_jointParamMaxMotor: return (M2_MAX_MOTOR_KINDS & (1u << type)) != 0 && m2ValidMaxForce(value); + case m2_jointParamMotorMaxForce: + return type == (uint8_t)m2_motorJoint && m2ValidMaxForce(value); case m2_jointParamBreakForce: case m2_jointParamBreakTorque: return m2ValidBreakLimit(value); @@ -350,6 +352,9 @@ static void ApplyJointParam(m2World* world, int32_t index, uint8_t param, float case m2_jointParamMaxMotor: j->jointMaxMotor[index] = value; break; + case m2_jointParamMotorMaxForce: + j->jointLength[index] = value; // the motor joint's force budget + break; case m2_jointParamEnableMotor: j->jointFlags[index] = value != 0.0f ? (j->jointFlags[index] | M2_JOINT_MOTOR) : (j->jointFlags[index] & ~M2_JOINT_MOTOR); @@ -453,7 +458,7 @@ static M2_NOINLINE void JointSetMotorSpeedWorker(m2World* world, m2JointId joint { return; } - m2SetJointParamInternal(world, jointId, m2_jointParamMotorSpeed, speed); + (void)m2SetJointParamInternal(world, jointId, m2_jointParamMotorSpeed, speed); } void m2Joint_SetMotorSpeed(m2World* world, m2JointId jointId, float speed) @@ -463,20 +468,20 @@ void m2Joint_SetMotorSpeed(m2World* world, m2JointId jointId, float speed) m2FloatEnvLeave(env); } -static M2_NOINLINE void JointSetMaxMotorWorker(m2World* world, m2JointId jointId, - float maxTorqueOrForce) +static M2_NOINLINE void JointSetMaxMotorEffortWorker(m2World* world, m2JointId jointId, + float maxEffort) { if (m2CheckWriter(world) != m2_success) { return; } - m2SetJointParamInternal(world, jointId, m2_jointParamMaxMotor, maxTorqueOrForce); + (void)m2SetJointParamInternal(world, jointId, m2_jointParamMaxMotor, maxEffort); } -void m2Joint_SetMaxMotor(m2World* world, m2JointId jointId, float maxTorqueOrForce) +void m2Joint_SetMaxMotorEffort(m2World* world, m2JointId jointId, float maxEffort) { uint64_t env = m2FloatEnvEnter(); - JointSetMaxMotorWorker(world, jointId, maxTorqueOrForce); + JointSetMaxMotorEffortWorker(world, jointId, maxEffort); m2FloatEnvLeave(env); } @@ -486,7 +491,7 @@ static M2_NOINLINE void JointEnableMotorWorker(m2World* world, m2JointId jointId { return; } - m2SetJointParamInternal(world, jointId, m2_jointParamEnableMotor, enable ? 1.0f : 0.0f); + (void)m2SetJointParamInternal(world, jointId, m2_jointParamEnableMotor, enable ? 1.0f : 0.0f); } void m2Joint_EnableMotor(m2World* world, m2JointId jointId, bool enable) @@ -502,7 +507,7 @@ static M2_NOINLINE void JointEnableLimitWorker(m2World* world, m2JointId jointId { return; } - m2SetJointParamInternal(world, jointId, m2_jointParamEnableLimit, enable ? 1.0f : 0.0f); + (void)m2SetJointParamInternal(world, jointId, m2_jointParamEnableLimit, enable ? 1.0f : 0.0f); } void m2Joint_EnableLimit(m2World* world, m2JointId jointId, bool enable) @@ -518,7 +523,7 @@ static M2_NOINLINE void JointEnableSpringWorker(m2World* world, m2JointId jointI { return; } - m2SetJointParamInternal(world, jointId, m2_jointParamEnableSpring, enable ? 1.0f : 0.0f); + (void)m2SetJointParamInternal(world, jointId, m2_jointParamEnableSpring, enable ? 1.0f : 0.0f); } void m2Joint_EnableSpring(m2World* world, m2JointId jointId, bool enable) @@ -535,7 +540,8 @@ static M2_NOINLINE void JointEnableConnectedCollisionWorker(m2World* world, m2Jo { return; } - m2SetJointParamInternal(world, jointId, m2_jointParamConnectedCollision, enable ? 1.0f : 0.0f); + (void)m2SetJointParamInternal(world, jointId, m2_jointParamConnectedCollision, + enable ? 1.0f : 0.0f); } void m2Joint_EnableConnectedCollision(m2World* world, m2JointId jointId, bool enable) @@ -564,7 +570,7 @@ static M2_NOINLINE void JointSetLimitsWorker(m2World* world, m2JointId jointId, } if (m2SetJointParamInternal(world, jointId, m2_jointParamLower, lower)) { - m2SetJointParamInternal(world, jointId, m2_jointParamUpper, upper); + (void)m2SetJointParamInternal(world, jointId, m2_jointParamUpper, upper); } } @@ -591,7 +597,7 @@ static M2_NOINLINE void JointSetTuningWorker(m2World* world, m2JointId jointId, } uint8_t param = second ? (damping ? m2_jointParamAngularDamping : m2_jointParamAngularHertz) : (damping ? m2_jointParamDamping : m2_jointParamHertz); - m2SetJointParamInternal(world, jointId, param, value); + (void)m2SetJointParamInternal(world, jointId, param, value); } void m2Joint_SetConstraintHertz(m2World* world, m2JointId jointId, float hertz) @@ -664,7 +670,7 @@ static M2_NOINLINE void JointSetBreakLimitsWorker(m2World* world, m2JointId join } if (m2SetJointParamInternal(world, jointId, m2_jointParamBreakForce, maxForce)) { - m2SetJointParamInternal(world, jointId, m2_jointParamBreakTorque, maxTorque); + (void)m2SetJointParamInternal(world, jointId, m2_jointParamBreakTorque, maxTorque); } } @@ -740,8 +746,8 @@ int32_t m2TypedJointSlot(const m2World* world, m2JointId jointId, uint8_t type) return index; } -static M2_NOINLINE bool JointIsConnectedCollisionEnabledWorker(const m2World* world, - m2JointId jointId) +M2_NODISCARD static M2_NOINLINE bool JointIsConnectedCollisionEnabledWorker(const m2World* world, + m2JointId jointId) { int32_t index = (int32_t)jointId.index1 - 1; if (world == NULL || index < 0 || index >= world->joints.jointCapacity || diff --git a/third_party/maul2d/src/joint.h b/third_party/maul2d/src/joint.h index bf6a4b128..152ed2149 100644 --- a/third_party/maul2d/src/joint.h +++ b/third_party/maul2d/src/joint.h @@ -19,6 +19,10 @@ #define M2_JOINT_HARD_RANGE 8u // distance: a finite length range is active #define M2_JOINT_ROPE 16u // distance: spring enabled (stored with the joint) #define M2_JOINT_FREE_LENGTH 32u // distance: zero-stiffness spring, skip the rest row +// Every bit above; a restore refuses any other. +#define M2_JOINT_FLAGS \ + (M2_JOINT_MOTOR | M2_JOINT_LIMIT | M2_JOINT_SPRING | M2_JOINT_HARD_RANGE | M2_JOINT_ROPE | \ + M2_JOINT_FREE_LENGTH) // Slot lookups: the live slot of a joint id, or -1. The typed lookup // also demands a joint type and refuses (world may be NULL) on a miss. @@ -87,7 +91,8 @@ float m2PulleyLiveLength(const m2World* world, int32_t index, int32_t side); // The journaled parameter channel (codes in journal.h) and the destroy // shared with the solver's break pass. -bool m2SetJointParamInternal(m2World* world, m2JointId jointId, uint8_t param, float value); +M2_NODISCARD bool m2SetJointParamInternal(m2World* world, m2JointId jointId, uint8_t param, + float value); void m2DestroyJointInternal(m2World* world, int32_t index); #endif // MAUL2D_SRC_JOINT_H diff --git a/third_party/maul2d/src/joint_distance.c b/third_party/maul2d/src/joint_distance.c index 43274f6a4..ecb174e55 100644 --- a/third_party/maul2d/src/joint_distance.c +++ b/third_party/maul2d/src/joint_distance.c @@ -157,7 +157,7 @@ static M2_NOINLINE void DistanceJointSetLengthWorker(m2World* world, m2JointId j { return; } - m2SetJointParamInternal(world, jointId, m2_jointParamLength, length); + (void)m2SetJointParamInternal(world, jointId, m2_jointParamLength, length); } void m2DistanceJoint_SetLength(m2World* world, m2JointId jointId, float length) @@ -186,7 +186,7 @@ static M2_NOINLINE void DistanceJointSetLengthRangeWorker(m2World* world, m2Join float upper = lo < hi ? hi : lo; if (m2SetJointParamInternal(world, jointId, m2_jointParamMinLength, lower)) { - m2SetJointParamInternal(world, jointId, m2_jointParamMaxLength, upper); + (void)m2SetJointParamInternal(world, jointId, m2_jointParamMaxLength, upper); } } diff --git a/third_party/maul2d/src/joint_edges.h b/third_party/maul2d/src/joint_edges.h index aff64c574..9fd50a9e2 100644 --- a/third_party/maul2d/src/joint_edges.h +++ b/third_party/maul2d/src/joint_edges.h @@ -17,6 +17,6 @@ void m2UnlinkJoint(m2World* world, int32_t joint); void m2RebuildJointEdges(m2World* world); // Whether a joint between the two bodies forbids their collision. -bool m2JointsForbidPair(const m2World* world, int32_t bodyA, int32_t bodyB); +M2_NODISCARD bool m2JointsForbidPair(const m2World* world, int32_t bodyA, int32_t bodyB); #endif // MAUL2D_SRC_JOINT_EDGES_H diff --git a/third_party/maul2d/src/joint_gear.c b/third_party/maul2d/src/joint_gear.c index 60f072e09..a5a6c340b 100644 --- a/third_party/maul2d/src/joint_gear.c +++ b/third_party/maul2d/src/joint_gear.c @@ -130,7 +130,7 @@ static M2_NOINLINE void GearJointSetRatioWorker(m2World* world, m2JointId jointI { return; } - m2SetJointParamInternal(world, jointId, m2_jointParamGearRatio, ratio); + (void)m2SetJointParamInternal(world, jointId, m2_jointParamGearRatio, ratio); } void m2GearJoint_SetRatio(m2World* world, m2JointId jointId, float ratio) diff --git a/third_party/maul2d/src/joint_motor.c b/third_party/maul2d/src/joint_motor.c index 1da0bc3d0..f21ee87a7 100644 --- a/third_party/maul2d/src/joint_motor.c +++ b/third_party/maul2d/src/joint_motor.c @@ -187,6 +187,37 @@ void m2MotorJoint_SetOffsets(m2World* world, m2JointId jointId, m2Vec2 linearOff m2FloatEnvLeave(env); } +// Both budgets ride the joint parameter channel; the type check comes +// first, so another joint type counts one misuse and the force channel +// and the shared motor budget take a motor joint only. +static M2_NOINLINE void MotorJointSetBudgetWorker(m2World* world, m2JointId jointId, uint8_t param, + float budget) +{ + if (m2CheckWriter(world) != m2_success) + { + return; + } + if (m2TypedJointSlot(world, jointId, (uint8_t)m2_motorJoint) < 0) + { + return; // TypedJointSlot refused + } + (void)m2SetJointParamInternal(world, jointId, param, budget); +} + +void m2MotorJoint_SetMaxForce(m2World* world, m2JointId jointId, float maxForce) +{ + uint64_t env = m2FloatEnvEnter(); + MotorJointSetBudgetWorker(world, jointId, m2_jointParamMotorMaxForce, maxForce); + m2FloatEnvLeave(env); +} + +void m2MotorJoint_SetMaxTorque(m2World* world, m2JointId jointId, float maxTorque) +{ + uint64_t env = m2FloatEnvEnter(); + MotorJointSetBudgetWorker(world, jointId, m2_jointParamMaxMotor, maxTorque); + m2FloatEnvLeave(env); +} + static M2_NOINLINE m2Vec2 MotorJointGetLinearOffsetWorker(const m2World* world, m2JointId jointId) { int32_t index = m2TypedJointSlot(world, jointId, (uint8_t)m2_motorJoint); @@ -230,6 +261,20 @@ float m2MotorJoint_GetMaxForce(const m2World* world, m2JointId jointId) return result; } +static M2_NOINLINE float MotorJointGetMaxTorqueWorker(const m2World* world, m2JointId jointId) +{ + int32_t index = m2TypedJointSlot(world, jointId, (uint8_t)m2_motorJoint); + return index >= 0 ? world->joints.jointMaxMotor[index] : 0.0f; +} + +float m2MotorJoint_GetMaxTorque(const m2World* world, m2JointId jointId) +{ + uint64_t env = m2FloatEnvEnter(); + float result = MotorJointGetMaxTorqueWorker(world, jointId); + m2FloatEnvLeave(env); + return result; +} + static M2_NOINLINE float MotorJointGetCorrectionFactorWorker(const m2World* world, m2JointId jointId) { diff --git a/third_party/maul2d/src/joint_mouse.c b/third_party/maul2d/src/joint_mouse.c index 41ed75331..063ed36bf 100644 --- a/third_party/maul2d/src/joint_mouse.c +++ b/third_party/maul2d/src/joint_mouse.c @@ -92,13 +92,17 @@ static M2_NOINLINE m2Result CreateMouseJointWorker(m2World* world, const m2Mouse // The grab point is where the target sits at creation, in B's // local frame (the single f64 crossing). It becomes B's anchor, a // body-local length, so it stays within M2_LOCAL_LIMIT like any other. + // The offset saturates as every world offset does, so a far target is + // m2_errorLimit at any rotation of B, never a NaN. It counts no misuse: + // how far the target lies from B depends on where the step left B, the + // world's state, not on an argument a correct program never passes. m2Transform xfB = world->bodies.transforms[bodyB]; - m2Vec2 rel = {(float)(def->target.x - xfB.p.x), (float)(def->target.y - xfB.p.y)}; + m2Vec2 rel = {m2WorldOffset(def->target.x - xfB.p.x), m2WorldOffset(def->target.y - xfB.p.y)}; m2Vec2 grab = {xfB.q.c * rel.x + xfB.q.s * rel.y, -xfB.q.s * rel.x + xfB.q.c * rel.y}; m2Result grabbed = m2CheckLocalVec2(grab); if (grabbed != m2_success) { - return m2Refuse(world, grabbed); + return grabbed; } int32_t index = m2AllocateJoint(world); if (index < 0) diff --git a/third_party/maul2d/src/joint_prismatic.c b/third_party/maul2d/src/joint_prismatic.c index 1fe5f01ef..73ceea2e2 100644 --- a/third_party/maul2d/src/joint_prismatic.c +++ b/third_party/maul2d/src/joint_prismatic.c @@ -38,7 +38,7 @@ static m2Result DefCheck(const m2PrismaticJointDef* def) result = m2BothChecks(result, m2CheckRate(def->constraintHertz)); result = m2BothChecks(result, m2CheckCoefficient(def->constraintDampingRatio)); result = m2BothChecks(result, m2CheckSpeed(def->motorSpeed)); - result = m2BothChecks(result, m2CheckMaxForce(def->maxMotorForce)); + result = m2BothChecks(result, m2CheckMaxForce(def->maxMotorEffort)); result = m2BothChecks(result, m2CheckReach(def->lowerTranslation)); result = m2BothChecks(result, m2CheckReach(def->upperTranslation)); return m2BothChecks(result, m2CheckValid(!(def->lowerTranslation > def->upperTranslation))); @@ -58,7 +58,7 @@ static void RecordPrismaticJointDef(m2PrismaticJointDef* out, const m2PrismaticJ out->constraintDampingRatio = def->constraintDampingRatio; out->enableMotor = def->enableMotor; out->motorSpeed = def->motorSpeed; - out->maxMotorForce = def->maxMotorForce; + out->maxMotorEffort = def->maxMotorEffort; out->enableLimit = def->enableLimit; out->lowerTranslation = def->lowerTranslation; out->upperTranslation = def->upperTranslation; @@ -123,7 +123,7 @@ static M2_NOINLINE m2Result CreatePrismaticJointWorker(m2World* world, world->joints.jointFlags[index] = (def->enableMotor ? M2_JOINT_MOTOR : 0u) | (def->enableLimit ? M2_JOINT_LIMIT : 0u); world->joints.jointMotorSpeed[index] = def->motorSpeed; - world->joints.jointMaxMotor[index] = def->maxMotorForce; + world->joints.jointMaxMotor[index] = def->maxMotorEffort; world->joints.jointLower[index] = def->lowerTranslation; world->joints.jointUpper[index] = def->upperTranslation; world->joints.jointLocalAxisA[index] = diff --git a/third_party/maul2d/src/joint_pulley.c b/third_party/maul2d/src/joint_pulley.c index 3f212b1ff..332ffa200 100644 --- a/third_party/maul2d/src/joint_pulley.c +++ b/third_party/maul2d/src/joint_pulley.c @@ -153,7 +153,7 @@ static M2_NOINLINE void PulleyJointSetRatioWorker(m2World* world, m2JointId join { return; } - m2SetJointParamInternal(world, jointId, m2_jointParamPulleyRatio, ratio); + (void)m2SetJointParamInternal(world, jointId, m2_jointParamPulleyRatio, ratio); } void m2PulleyJoint_SetRatio(m2World* world, m2JointId jointId, float ratio) diff --git a/third_party/maul2d/src/joint_ratchet.c b/third_party/maul2d/src/joint_ratchet.c index e36a9aa45..3991dad29 100644 --- a/third_party/maul2d/src/joint_ratchet.c +++ b/third_party/maul2d/src/joint_ratchet.c @@ -23,7 +23,7 @@ m2RatchetJointDef m2DefaultRatchetJointDef(void) { m2RatchetJointDef def; memset(&def, 0, sizeof(def)); - def.ratchet = 0.5f; + def.toothAngle = 0.5f; def.internalValue = M2_RTJOINT_COOKIE; return def; } @@ -37,7 +37,7 @@ m2RatchetJointDef m2DefaultRatchetJointDef(void) // ordered ranges. static m2Result DefCheck(const m2RatchetJointDef* def) { - return m2BothChecks(m2CheckRatchetTooth(def->ratchet), m2CheckAngle(def->phase)); + return m2BothChecks(m2CheckRatchetTooth(def->toothAngle), m2CheckAngle(def->phase)); } // The create record holds the def's fields and zero bytes between them: @@ -47,7 +47,7 @@ static void RecordRatchetJointDef(m2RatchetJointDef* out, const m2RatchetJointDe { out->bodyIdA = def->bodyIdA; out->bodyIdB = def->bodyIdB; - out->ratchet = def->ratchet; + out->toothAngle = def->toothAngle; out->phase = def->phase; out->userData = def->userData; out->enableConnectedCollision = def->enableConnectedCollision; @@ -94,7 +94,7 @@ static M2_NOINLINE m2Result CreateRatchetJointWorker(m2World* world, const m2Rat m2Vec2 zero = {0.0f, 0.0f}; m2JointId jointId = m2FinishJoint(world, index, (uint8_t)m2_ratchetJoint, bodyA, bodyB, zero, zero, 0.0f, 0.0f, 0.0f); - world->joints.jointLength[index] = def->ratchet; + world->joints.jointLength[index] = def->toothAngle; world->joints.jointRefAngle[index] = def->phase; m2Rot qA = world->bodies.transforms[bodyA].q; m2Rot qB = world->bodies.transforms[bodyB].q; @@ -103,7 +103,7 @@ static M2_NOINLINE m2Result CreateRatchetJointWorker(m2World* world, const m2Rat world->joints.jointUpper[index] = 0.0f; // accumulated relative angle // Engage the tooth at or behind the angle at creation. world->joints.jointLower[index] = - floorf((0.0f - def->phase) / def->ratchet) * def->ratchet + def->phase; + floorf((0.0f - def->phase) / def->toothAngle) * def->toothAngle + def->phase; world->joints.jointUserData[index] = def->userData; world->joints.jointCollide[index] = def->enableConnectedCollision ? 1 : 0; if (def->enableConnectedCollision == false) @@ -130,16 +130,16 @@ m2Result m2CreateRatchetJoint(m2World* world, const m2RatchetJointDef* def, m2Jo return result; } -static M2_NOINLINE float RatchetJointGetRatchetWorker(const m2World* world, m2JointId jointId) +static M2_NOINLINE float RatchetJointGetToothAngleWorker(const m2World* world, m2JointId jointId) { int32_t index = m2TypedJointSlot(world, jointId, (uint8_t)m2_ratchetJoint); return index >= 0 ? world->joints.jointLength[index] : 0.0f; } -float m2RatchetJoint_GetRatchet(const m2World* world, m2JointId jointId) +float m2RatchetJoint_GetToothAngle(const m2World* world, m2JointId jointId) { uint64_t env = m2FloatEnvEnter(); - float result = RatchetJointGetRatchetWorker(world, jointId); + float result = RatchetJointGetToothAngleWorker(world, jointId); m2FloatEnvLeave(env); return result; } diff --git a/third_party/maul2d/src/joint_read.c b/third_party/maul2d/src/joint_read.c index 338c0e035..c516c98fc 100644 --- a/third_party/maul2d/src/joint_read.c +++ b/third_party/maul2d/src/joint_read.c @@ -202,16 +202,16 @@ float m2Joint_GetMotorSpeed(const m2World* world, m2JointId jointId) return result; } -static M2_NOINLINE float JointGetMaxMotorWorker(const m2World* world, m2JointId jointId) +static M2_NOINLINE float JointGetMaxMotorEffortWorker(const m2World* world, m2JointId jointId) { int32_t index = JointSlotOfKind(world, jointId, M2_MAX_MOTOR_KINDS); return index >= 0 ? world->joints.jointMaxMotor[index] : 0.0f; } -float m2Joint_GetMaxMotor(const m2World* world, m2JointId jointId) +float m2Joint_GetMaxMotorEffort(const m2World* world, m2JointId jointId) { uint64_t env = m2FloatEnvEnter(); - float result = JointGetMaxMotorWorker(world, jointId); + float result = JointGetMaxMotorEffortWorker(world, jointId); m2FloatEnvLeave(env); return result; } diff --git a/third_party/maul2d/src/joint_revolute.c b/third_party/maul2d/src/joint_revolute.c index c6a797dcf..a4b1c1fc7 100644 --- a/third_party/maul2d/src/joint_revolute.c +++ b/third_party/maul2d/src/joint_revolute.c @@ -38,7 +38,7 @@ static m2Result DefCheck(const m2RevoluteJointDef* def) result = m2BothChecks(result, m2CheckRate(def->springHertz)); result = m2BothChecks(result, m2CheckCoefficient(def->springDampingRatio)); result = m2BothChecks(result, m2CheckSpeed(def->motorSpeed)); - result = m2BothChecks(result, m2CheckMaxForce(def->maxMotorTorque)); + result = m2BothChecks(result, m2CheckMaxForce(def->maxMotorEffort)); result = m2BothChecks(result, m2CheckAngle(def->lowerAngle)); result = m2BothChecks(result, m2CheckAngle(def->upperAngle)); return m2BothChecks(result, m2CheckValid(!(def->lowerAngle > def->upperAngle))); @@ -60,7 +60,7 @@ static void RecordRevoluteJointDef(m2RevoluteJointDef* out, const m2RevoluteJoin out->enableSpring = def->enableSpring; out->enableMotor = def->enableMotor; out->motorSpeed = def->motorSpeed; - out->maxMotorTorque = def->maxMotorTorque; + out->maxMotorEffort = def->maxMotorEffort; out->enableLimit = def->enableLimit; out->lowerAngle = def->lowerAngle; out->upperAngle = def->upperAngle; @@ -119,7 +119,7 @@ static M2_NOINLINE m2Result CreateRevoluteJointWorker(m2World* world, const m2Re (def->enableLimit ? M2_JOINT_LIMIT : 0u) | (def->enableSpring ? M2_JOINT_SPRING : 0u); world->joints.jointMotorSpeed[index] = def->motorSpeed; - world->joints.jointMaxMotor[index] = def->maxMotorTorque; + world->joints.jointMaxMotor[index] = def->maxMotorEffort; world->joints.jointLower[index] = def->lowerAngle; world->joints.jointUpper[index] = def->upperAngle; world->joints.jointRefAngle[index] = diff --git a/third_party/maul2d/src/joint_row.c b/third_party/maul2d/src/joint_row.c index c94c4c3f7..9527c008b 100644 --- a/third_party/maul2d/src/joint_row.c +++ b/third_party/maul2d/src/joint_row.c @@ -247,7 +247,7 @@ typedef struct m2WeldMass // near-equal products: det K = m^2 (iA + iB) + m iA iB |a - b|^2, and each // cofactor is expanded the same way. Returns false when the block is // singular (neither body turns) or too stiff to multiply. -static bool WeldMass(m2Vec2 armA, m2Vec2 armB, const m2JointBodies* b, m2WeldMass* out) +M2_NODISCARD static bool WeldMass(m2Vec2 armA, m2Vec2 armB, const m2JointBodies* b, m2WeldMass* out) { double ax = (double)armA.x; double ay = (double)armA.y; diff --git a/third_party/maul2d/src/joint_row.h b/third_party/maul2d/src/joint_row.h index 51ba3bffa..b0127f040 100644 --- a/third_party/maul2d/src/joint_row.h +++ b/third_party/maul2d/src/joint_row.h @@ -158,8 +158,9 @@ void m2SolveSpringPair(m2Vec2 armA, m2Vec2 armB, m2JointBodies* b, m2Vec2 gap, f // its inertia, since they are then coupled almost one to one. Returns // false, changing nothing, when the block is singular (neither body // turns); the caller then solves the rows apart. -bool m2SolveWeldBlock(m2Vec2 armA, m2Vec2 armB, m2JointBodies* b, m2Vec2 bias, float angleBias, - m2RowDrive drive, m2Vec2* accumulated, float* accumulatedAngle); +M2_NODISCARD bool m2SolveWeldBlock(m2Vec2 armA, m2Vec2 armB, m2JointBodies* b, m2Vec2 bias, + float angleBias, m2RowDrive drive, m2Vec2* accumulated, + float* accumulatedAngle); // Pushes a weld's point impulse and angle impulse together, each body's // spin changed once by their summed torque. diff --git a/third_party/maul2d/src/joint_solver.c b/third_party/maul2d/src/joint_solver.c index 281ceb9b5..142892135 100644 --- a/third_party/maul2d/src/joint_solver.c +++ b/third_party/maul2d/src/joint_solver.c @@ -29,13 +29,13 @@ m2Softness m2StiffJointSoftness(float h) return m2MakeSoft(M2_JOINT_HERTZ, M2_JOINT_DAMPING_RATIO, h); } -static bool Awake(const m2World* world, int32_t body) +M2_NODISCARD static bool Awake(const m2World* world, int32_t body) { return world->bodies.types[body] == (uint8_t)m2_dynamicBody && world->bodies.asleep[body] == 0; } // True when the joint has rows to solve this step. -static bool JointSolves(const m2World* world, int32_t j) +M2_NODISCARD static bool JointSolves(const m2World* world, int32_t j) { int32_t bodyA = world->joints.jointBodyA[j]; int32_t bodyB = world->joints.jointBodyB[j]; diff --git a/third_party/maul2d/src/joint_wheel.c b/third_party/maul2d/src/joint_wheel.c index d45a9f530..d7f8731db 100644 --- a/third_party/maul2d/src/joint_wheel.c +++ b/third_party/maul2d/src/joint_wheel.c @@ -41,7 +41,7 @@ static m2Result DefCheck(const m2WheelJointDef* def) result = m2BothChecks(result, m2CheckRate(def->springHertz)); result = m2BothChecks(result, m2CheckCoefficient(def->springDampingRatio)); result = m2BothChecks(result, m2CheckSpeed(def->motorSpeed)); - result = m2BothChecks(result, m2CheckMaxForce(def->maxMotorTorque)); + result = m2BothChecks(result, m2CheckMaxForce(def->maxMotorEffort)); result = m2BothChecks(result, m2CheckReach(def->lowerTranslation)); result = m2BothChecks(result, m2CheckReach(def->upperTranslation)); return m2BothChecks(result, m2CheckValid(!(def->lowerTranslation > def->upperTranslation))); @@ -62,7 +62,7 @@ static void RecordWheelJointDef(m2WheelJointDef* out, const m2WheelJointDef* def out->springDampingRatio = def->springDampingRatio; out->enableMotor = def->enableMotor; out->motorSpeed = def->motorSpeed; - out->maxMotorTorque = def->maxMotorTorque; + out->maxMotorEffort = def->maxMotorEffort; out->enableLimit = def->enableLimit; out->lowerTranslation = def->lowerTranslation; out->upperTranslation = def->upperTranslation; @@ -127,7 +127,7 @@ static M2_NOINLINE m2Result CreateWheelJointWorker(m2World* world, const m2Wheel (def->enableLimit ? M2_JOINT_LIMIT : 0u) | (def->enableSpring ? M2_JOINT_SPRING : 0u); world->joints.jointMotorSpeed[index] = def->motorSpeed; - world->joints.jointMaxMotor[index] = def->maxMotorTorque; + world->joints.jointMaxMotor[index] = def->maxMotorEffort; world->joints.jointLower[index] = def->lowerTranslation; world->joints.jointUpper[index] = def->upperTranslation; world->joints.jointLocalAxisA[index] = diff --git a/third_party/maul2d/src/journal.c b/third_party/maul2d/src/journal.c index f4411e5d9..508b87ffb 100644 --- a/third_party/maul2d/src/journal.c +++ b/third_party/maul2d/src/journal.c @@ -20,7 +20,7 @@ #include #define M2_JOURNAL_MAGIC M2_STATE_MAGIC('M', '2', 'J', 'N') -#define M2_JOURNAL_VERSION 8u +#define M2_JOURNAL_VERSION 10u // The world's shape travels in the embedded snapshot's header, which // restore checks; the tape's header only says where the records start. @@ -129,7 +129,7 @@ void m2JournalRecordChain(m2World* world, m2BodyId bodyId, const m2ChainDef* def static M2_NOINLINE int64_t WorldGetJournalBaseSizeWorker(const m2World* world) { - return world != NULL ? (int64_t)sizeof(m2JournalHeader) + m2World_SnapshotSize(world) : 0; + return world != NULL ? (int64_t)sizeof(m2JournalHeader) + m2World_GetSnapshotSize(world) : 0; } int64_t m2World_GetJournalBaseSize(const m2World* world) @@ -147,7 +147,7 @@ static M2_NOINLINE m2Result WorldStartJournalWorker(m2World* world, void* buffer { return writer; } - if (buffer == NULL || !m2SizeFitsMemory(capacity)) + if (buffer == NULL || capacity < 0 || !m2SizeFitsMemory(capacity)) { return m2Refuse(world, m2_errorInvalid); } @@ -155,7 +155,7 @@ static M2_NOINLINE m2Result WorldStartJournalWorker(m2World* world, void* buffer { return m2Refuse(world, m2_errorState); } - int64_t snapshotSize = m2World_SnapshotSize(world); + int64_t snapshotSize = m2World_GetSnapshotSize(world); if (capacity < (int64_t)sizeof(m2JournalHeader) + snapshotSize) { return m2Refuse(world, m2_errorCapacity); @@ -192,12 +192,18 @@ m2Result m2World_StartJournal(m2World* world, void* buffer, int64_t capacity) static M2_NOINLINE m2Result WorldStopJournalWorker(m2World* world, int64_t* bytesOut) { + // A busy refusal leaves bytesOut, which host code the world is running + // may be filling; every other failure clears it. + m2Result writer = m2CheckWriter(world); + if (writer == m2_errorBusy) + { + return writer; + } if (bytesOut == NULL) { return m2Refuse(world, m2_errorInvalid); } *bytesOut = 0; - m2Result writer = m2CheckWriter(world); if (writer != m2_success) { return writer; @@ -229,6 +235,120 @@ m2Result m2World_StopJournal(m2World* world, int64_t* bytesOut) return result; } +// The event streams are observer state no snapshot holds, and the restore +// that backs out a refused replay clears them. The replay keeps a copy, so +// a refused tape leaves the last step's events readable and an end event a +// between-step destroy queued still reaches the next step. +enum +{ + M2_EVENT_STREAMS = 7 +}; + +typedef struct m2EventStream +{ + void* items; + int32_t* count; + size_t itemSize; +} m2EventStream; + +static void EventStreams(m2World* world, m2EventStream streams[M2_EVENT_STREAMS]) +{ + m2Events* e = &world->events; + streams[0] = (m2EventStream){e->beginEvents, &e->beginEventCount, sizeof(*e->beginEvents)}; + streams[1] = (m2EventStream){e->endEvents, &e->endEventCount, sizeof(*e->endEvents)}; + streams[2] = + (m2EventStream){e->pendingEndEvents, &e->pendingEndCount, sizeof(*e->pendingEndEvents)}; + streams[3] = + (m2EventStream){e->sensorBeginEvents, &e->sensorBeginCount, sizeof(*e->sensorBeginEvents)}; + streams[4] = + (m2EventStream){e->sensorEndEvents, &e->sensorEndCount, sizeof(*e->sensorEndEvents)}; + streams[5] = (m2EventStream){e->pendingSensorEnd, &e->pendingSensorEndCount, + sizeof(*e->pendingSensorEnd)}; + streams[6] = (m2EventStream){e->jointBreakEvents, &e->jointBreakEventCount, + sizeof(*e->jointBreakEvents)}; +} + +// The bytes the streams' events take. Each stream lives in the world's own +// memory, so the sum fits a size_t. +static size_t EventBytes(const m2EventStream streams[M2_EVENT_STREAMS]) +{ + size_t bytes = 0; + for (int32_t i = 0; i < M2_EVENT_STREAMS; ++i) + { + bytes += (size_t)*streams[i].count * streams[i].itemSize; + } + return bytes; +} + +static void SaveEvents(const m2EventStream streams[M2_EVENT_STREAMS], uint8_t* out, + int32_t counts[M2_EVENT_STREAMS]) +{ + for (int32_t i = 0; i < M2_EVENT_STREAMS; ++i) + { + size_t bytes = (size_t)*streams[i].count * streams[i].itemSize; + counts[i] = *streams[i].count; + if (bytes > 0) + { + memcpy(out, streams[i].items, bytes); + } + out += bytes; + } +} + +static void LoadEvents(const m2EventStream streams[M2_EVENT_STREAMS], const uint8_t* in, + const int32_t counts[M2_EVENT_STREAMS]) +{ + for (int32_t i = 0; i < M2_EVENT_STREAMS; ++i) + { + size_t bytes = (size_t)counts[i] * streams[i].itemSize; + if (bytes > 0) + { + memcpy(streams[i].items, in, bytes); + } + *streams[i].count = counts[i]; + in += bytes; + } +} + +// The counts of m2Counters that steps and creates set or add to, which no +// snapshot holds either. A refused replay puts them back after its steps +// ran, so it leaves them as they were before the call. The misuse count is +// not among them: what host code the replay ran gets wrong still counts. +typedef struct m2ObserverCounts +{ + int32_t constraintCount; + int32_t colorCount; + int32_t overflowCount; + int32_t pairsDropped; + int32_t particlePairsDropped; + int32_t particleBodyContactsDropped; + uint64_t particleCreatesRefused; +} m2ObserverCounts; + +static m2ObserverCounts SaveCounts(const m2World* world) +{ + m2ObserverCounts counts; + counts.constraintCount = world->solver.lastConstraintCount; + counts.colorCount = world->solver.lastGraphColors; + counts.overflowCount = world->solver.lastOverflow; + counts.pairsDropped = world->contacts.pairsDropped; + counts.particlePairsDropped = world->particles.particlePairsDropped; + counts.particleBodyContactsDropped = world->particles.particleBodyContactsDropped; + counts.particleCreatesRefused = world->particles.particleCreatesRefused; + return counts; +} + +static void LoadCounts(m2World* world, const m2ObserverCounts* counts) +{ + world->solver.lastConstraintCount = counts->constraintCount; + world->solver.lastGraphColors = counts->colorCount; + world->solver.lastOverflow = counts->overflowCount; + world->contacts.pairsDropped = counts->pairsDropped; + world->particles.particlePairsDropped = counts->particlePairsDropped; + world->particles.particleBodyContactsDropped = counts->particleBodyContactsDropped; + world->particles.particleCreatesRefused = counts->particleCreatesRefused; +} + // Applies a tape to a world: header, snapshot, then one command per op. // Stops at the first op that is malformed, unknown, or recreates an // object under a different id than the recording saw; the caller rolls @@ -242,8 +362,8 @@ static m2Result ReplayOps(m2World* world, const uint8_t* data, int64_t size) return m2_errorFormat; } // The embedded snapshot's refusal is the tape's: m2_errorVersion for - // one from another release or build, m2_errorFormat for the rest, one - // from another world shape included. + // one from another minor version or build, m2_errorFormat for the + // rest, one from another world shape included. m2Result restored = m2World_Restore(world, data + sizeof(header), header.snapshotSize); if (restored != m2_success) { @@ -279,10 +399,15 @@ static m2Result ReplayOps(m2World* world, const uint8_t* data, int64_t size) } memcpy(&payload, data + cursor.offset, (size_t)command->payloadSize); cursor.offset += command->payloadSize; + world->tapeRefused = 0; if (!command->apply(&cursor, &payload)) { return cursor.failure; } + if (world->tapeRefused != 0) + { + return m2_errorFormat; // a void call the tape's values made refuse + } } return m2_success; } @@ -303,8 +428,8 @@ static M2_NOINLINE m2Result WorldReplayJournalWorker(m2World* world, const void* return m2Refuse(world, m2_errorState); } // From here on the tape is untrusted input: refusing it is no misuse - // of the API, so nothing is counted. Only the release that wrote a - // tape replays it. + // of the API, so nothing is counted. Only the minor version that wrote + // a tape replays it. m2Result prefix = m2CheckStatePrefix(data, size, M2_JOURNAL_MAGIC, M2_JOURNAL_VERSION, (int32_t)sizeof(m2JournalHeader)); if (prefix != m2_success) @@ -312,10 +437,15 @@ static M2_NOINLINE m2Result WorldReplayJournalWorker(m2World* world, const void* return prefix; } // Atomic: the world takes the whole tape or none of it. A snapshot - // taken first backs out a replay that fails part way, so a corrupt - // or truncated tape never leaves a half-built world behind. - int64_t backupSize = m2World_SnapshotSize(world); - void* backup = m2WorldAlloc(world, (size_t)backupSize); + // taken first, and copies of the event streams and the observer counts + // after it, back out a replay that fails part way, so a corrupt or + // truncated tape never leaves a half-built world behind. + int64_t backupSize = m2World_GetSnapshotSize(world); + m2EventStream streams[M2_EVENT_STREAMS]; + int32_t eventCounts[M2_EVENT_STREAMS]; + EventStreams(world, streams); + size_t scratchBytes = (size_t)backupSize + EventBytes(streams); + uint8_t* backup = m2WorldAlloc(world, scratchBytes); if (backup == NULL) { return m2Refuse(world, m2_errorCapacity); // no memory for the guarantee means no replay @@ -324,6 +454,8 @@ static M2_NOINLINE m2Result WorldReplayJournalWorker(m2World* world, const void* m2Result snapshot = m2World_Snapshot(world, backup, backupSize, &written); M2_ASSERT(snapshot == m2_success && written == backupSize); // sized just above (void)snapshot; + SaveEvents(streams, backup + backupSize, eventCounts); + m2ObserverCounts counts = SaveCounts(world); // The ops re-enter the public calls with the tape's values; a value // they refuse is the tape's fault, not the caller's, and counts nothing // (m2Refuse). Host code the replay runs is the caller's and still @@ -335,9 +467,11 @@ static M2_NOINLINE m2Result WorldReplayJournalWorker(m2World* world, const void* m2Result restored = m2World_Restore(world, backup, backupSize); M2_ASSERT(restored == m2_success); // our own snapshot must restore: invariant (void)restored; + LoadEvents(streams, backup + backupSize, eventCounts); + LoadCounts(world, &counts); } world->replaying = 0; - m2WorldFree(world, backup, (size_t)backupSize); + m2WorldFree(world, backup, scratchBytes); return result; } diff --git a/third_party/maul2d/src/journal.h b/third_party/maul2d/src/journal.h index 74f4f395c..84ea9793c 100644 --- a/third_party/maul2d/src/journal.h +++ b/third_party/maul2d/src/journal.h @@ -84,10 +84,11 @@ enum m2_opShatterBody = 58, // variable length: m2OpShatterHeader + polygons m2_opSetParticleLifetime = 59, m2_opSetParticleUserData = 60, - m2_opCreateFluidVolume = 61, - m2_opDestroyFluidVolume = 62, - m2_opSetFluidSurface = 63, + m2_opCreateWaterVolume = 61, + m2_opDestroyWaterVolume = 62, + m2_opSetWaterSurface = 63, m2_opSetWind = 64, + m2_opWaterUserData = 65, m2_opCount }; @@ -102,6 +103,7 @@ enum m2_bodyParamEnableSleep = 4, m2_bodyParamLockLinearX = 5, m2_bodyParamLockLinearY = 6, + m2_bodyParamSleepThreshold = 7, m2_bodyParamCount }; @@ -134,6 +136,7 @@ enum m2_jointParamPulleyRatio = 16, m2_jointParamEnableSpring = 17, // distance and wheel; resets the spring's impulses m2_jointParamConnectedCollision = 18, // every kind but the filter joint + m2_jointParamMotorMaxForce = 19, // motor only m2_jointParamCount }; @@ -354,17 +357,23 @@ typedef struct m2OpParticleUserData M2_ALIGN8 uint64_t userData; } m2OpParticleUserData; -typedef struct m2OpCreateFluidVolume +typedef struct m2OpCreateWaterVolume { - m2FluidVolumeDef def; - m2FluidVolumeId expected; -} m2OpCreateFluidVolume; + m2WaterVolumeDef def; + m2WaterVolumeId expected; +} m2OpCreateWaterVolume; -typedef struct m2OpFluidSurface +typedef struct m2OpWaterSurface { - m2FluidVolumeId id; + m2WaterVolumeId id; M2_ALIGN8 double surface; -} m2OpFluidSurface; +} m2OpWaterSurface; + +typedef struct m2OpWaterUserData +{ + m2WaterVolumeId id; + M2_ALIGN8 uint64_t userData; +} m2OpWaterUserData; typedef struct m2OpShatterHeader { @@ -431,8 +440,9 @@ typedef union m2OpPayload m2OpParticleVec particleVec; m2OpParticleFloat particleFloat; m2OpParticleUserData particleUserData; - m2OpCreateFluidVolume createFluidVolume; - m2OpFluidSurface fluidSurface; + m2OpCreateWaterVolume createWaterVolume; + m2OpWaterSurface waterSurface; + m2OpWaterUserData waterUserData; m2OpShatterHeader shatterHeader; m2OpFlag flag; m2OpVec vec; @@ -443,7 +453,7 @@ typedef union m2OpPayload m2ChainId chainId; m2JointId jointId; m2ParticleId particleId; - m2FluidVolumeId fluidVolumeId; + m2WaterVolumeId waterVolumeId; int64_t restoreSize; } m2OpPayload; @@ -452,7 +462,7 @@ typedef union m2OpPayload // Whether a record of 1 + bytes fits a tape of capacity bytes written up // to cursor (0 <= cursor <= capacity). -bool m2JournalRecordFits(int64_t cursor, int64_t capacity, int64_t bytes); +M2_NODISCARD bool m2JournalRecordFits(int64_t cursor, int64_t capacity, int64_t bytes); void m2JournalRecord(m2World* world, uint8_t op, const void* payload, int32_t bytes); void m2JournalRecordRestore(m2World* world, const void* snapshot, int64_t size); void m2JournalRecordShatter(m2World* world, m2BodyId bodyId, const m2Polygon* pieces, @@ -479,8 +489,8 @@ typedef bool m2ReplayApplyFn(m2ReplayCursor* cursor, const m2OpPayload* payload) // Whether a record's tail of count elements of elementBytes each lies // within the tape after the cursor; the tail's size, reckoned in 64 bits // so no count can wrap it, goes to bytesOut. -bool m2ReplayTailFits(const m2ReplayCursor* r, int32_t count, int32_t elementBytes, - int64_t* bytesOut); +M2_NODISCARD bool m2ReplayTailFits(const m2ReplayCursor* r, int32_t count, int32_t elementBytes, + int64_t* bytesOut); typedef struct m2JournalCommand { diff --git a/third_party/maul2d/src/journal_replay.c b/third_party/maul2d/src/journal_replay.c index 95b7eca7d..cf4f8798b 100644 --- a/third_party/maul2d/src/journal_replay.c +++ b/third_party/maul2d/src/journal_replay.c @@ -22,135 +22,134 @@ // Ids are compared by slot and generation. #define M2_SAME_ID(a, b) ((a).index1 == (b).index1 && (a).generation == (b).generation) -static bool ApplyStep(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyStep(m2ReplayCursor* r, const m2OpPayload* p) { - m2World_Step(r->world, p->step.dt, p->step.substepCount); - return true; + return m2World_Step(r->world, p->step.dt, p->step.substepCount) == m2_success; } -static bool ApplyCreateBody(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyCreateBody(m2ReplayCursor* r, const m2OpPayload* p) { m2BodyId id = m2_nullBodyId; return m2CreateBody(r->world, &p->createBody.def, &id) == m2_success && M2_SAME_ID(id, p->createBody.expected); } -static bool ApplyDestroyBody(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyDestroyBody(m2ReplayCursor* r, const m2OpPayload* p) { return m2DestroyBody(r->world, p->bodyId) == m2_success; } -static bool ApplyDisableBody(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyDisableBody(m2ReplayCursor* r, const m2OpPayload* p) { - m2Body_SetEnabled(r->world, p->bodyId, false); + m2Body_Enable(r->world, p->bodyId, false); return true; } -static bool ApplyEnableBody(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyEnableBody(m2ReplayCursor* r, const m2OpPayload* p) { - m2Body_SetEnabled(r->world, p->bodyId, true); + m2Body_Enable(r->world, p->bodyId, true); return true; } -static bool ApplyMassFromShapes(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyMassFromShapes(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_ApplyMassFromShapes(r->world, p->bodyId); return true; } -static bool ApplySetLinearVelocity(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetLinearVelocity(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_SetLinearVelocity(r->world, p->bodyVec.body, p->bodyVec.value); return true; } -static bool ApplyApplyForceCenter(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyApplyForceCenter(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_ApplyForce(r->world, p->bodyVec.body, p->bodyVec.value); return true; } -static bool ApplyImpulseCenter(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyImpulseCenter(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_ApplyLinearImpulse(r->world, p->bodyVec.body, p->bodyVec.value); return true; } -static bool ApplySetAngularVelocity(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetAngularVelocity(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_SetAngularVelocity(r->world, p->bodyFloat.body, p->bodyFloat.value); return true; } -static bool ApplyApplyTorque(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyApplyTorque(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_ApplyTorque(r->world, p->bodyFloat.body, p->bodyFloat.value); return true; } -static bool ApplyAngularImpulse(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyAngularImpulse(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_ApplyAngularImpulse(r->world, p->bodyFloat.body, p->bodyFloat.value); return true; } -static bool ApplyLinearImpulse(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyLinearImpulse(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_ApplyLinearImpulseAtPoint(r->world, p->bodyPoint.body, p->bodyPoint.value, p->bodyPoint.point); return true; } -static bool ApplyApplyForce(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyApplyForce(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_ApplyForceAtPoint(r->world, p->bodyPoint.body, p->bodyPoint.value, p->bodyPoint.point); return true; } -static bool ApplyBodyParam(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyBodyParam(m2ReplayCursor* r, const m2OpPayload* p) { return m2SetBodyParamInternal(r->world, p->bodyParam.body, p->bodyParam.param, p->bodyParam.value); } -static bool ApplySetType(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetType(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_SetType(r->world, p->bodyByte.body, (m2BodyType)p->bodyByte.value); return true; } -static bool ApplySetAwake(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetAwake(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_SetAwake(r->world, p->bodyByte.body, p->bodyByte.value != 0); return true; } -static bool ApplySetBullet(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetBullet(m2ReplayCursor* r, const m2OpPayload* p) { - m2Body_SetBullet(r->world, p->bodyByte.body, p->bodyByte.value != 0); + m2Body_EnableBullet(r->world, p->bodyByte.body, p->bodyByte.value != 0); return true; } -static bool ApplySetDominance(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetDominance(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_SetDominance(r->world, p->bodyByte.body, (int8_t)p->bodyByte.value); return true; } -static bool ApplyBodyUserData(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyBodyUserData(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_SetUserData(r->world, p->bodyUserData.body, p->bodyUserData.userData); return true; } -static bool ApplySetTransform(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetTransform(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_SetTransform(r->world, p->setTransform.body, p->setTransform.position, p->setTransform.rotation); return true; } -static bool ApplySetMassData(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetMassData(m2ReplayCursor* r, const m2OpPayload* p) { m2Body_SetMassData(r->world, p->setMassData.body, p->setMassData.data); return true; @@ -158,7 +157,7 @@ static bool ApplySetMassData(m2ReplayCursor* r, const m2OpPayload* p) // Shapes. -static bool ApplyCreateShape(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyCreateShape(m2ReplayCursor* r, const m2OpPayload* p) { const m2OpCreateShape* op = &p->createShape; m2BodyId body = op->body; @@ -184,31 +183,31 @@ static bool ApplyCreateShape(m2ReplayCursor* r, const m2OpPayload* p) return made == m2_success && M2_SAME_ID(id, op->expected); } -static bool ApplyDestroyShape(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyDestroyShape(m2ReplayCursor* r, const m2OpPayload* p) { return m2DestroyShape(r->world, p->shapeId) == m2_success; } -static bool ApplyShapeParam(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyShapeParam(m2ReplayCursor* r, const m2OpPayload* p) { return m2SetShapeParamInternal(r->world, p->shapeParam.shape, p->shapeParam.param, p->shapeParam.value); } -static bool ApplySetDensity(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetDensity(m2ReplayCursor* r, const m2OpPayload* p) { m2Shape_SetDensity(r->world, p->shapeFloat.shape, p->shapeFloat.value); return true; } -static bool ApplySetFilter(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetFilter(m2ReplayCursor* r, const m2OpPayload* p) { const m2OpSetFilter* op = &p->setFilter; m2Shape_SetFilter(r->world, op->shape, op->categoryBits, op->maskBits, op->groupIndex); return true; } -static bool ApplySetGeometry(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetGeometry(m2ReplayCursor* r, const m2OpPayload* p) { const m2OpSetGeometry* op = &p->setGeometry; m2ShapeId shape = op->shape; @@ -231,7 +230,7 @@ static bool ApplySetGeometry(m2ReplayCursor* r, const m2OpPayload* p) } } -static bool ApplyShapeUserData(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyShapeUserData(m2ReplayCursor* r, const m2OpPayload* p) { m2Shape_SetUserData(r->world, p->shapeUserData.shape, p->shapeUserData.userData); return true; @@ -247,7 +246,7 @@ bool m2ReplayTailFits(const m2ReplayCursor* r, int32_t count, int32_t elementByt // Chains. -static bool ApplyCreateChain(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyCreateChain(m2ReplayCursor* r, const m2OpPayload* p) { const m2OpChainHeader* op = &p->chainHeader; int64_t pointBytes = 0; @@ -281,18 +280,18 @@ static bool ApplyCreateChain(m2ReplayCursor* r, const m2OpPayload* p) m2Chain_GetSegmentCount(r->world, chain) == op->createdCount; } -static bool ApplyDestroyChain(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyDestroyChain(m2ReplayCursor* r, const m2OpPayload* p) { return m2DestroyChain(r->world, p->chainId) == m2_success; } -static bool ApplyChainFriction(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyChainFriction(m2ReplayCursor* r, const m2OpPayload* p) { m2Chain_SetFriction(r->world, p->chainFloat.chain, p->chainFloat.value); return true; } -static bool ApplyChainRestitution(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyChainRestitution(m2ReplayCursor* r, const m2OpPayload* p) { m2Chain_SetRestitution(r->world, p->chainFloat.chain, p->chainFloat.value); return true; @@ -324,37 +323,37 @@ M2_APPLY_CREATE_JOINT(Ratchet) #undef M2_APPLY_CREATE_JOINT -static bool ApplyDestroyJoint(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyDestroyJoint(m2ReplayCursor* r, const m2OpPayload* p) { return m2DestroyJoint(r->world, p->jointId) == m2_success; } -static bool ApplySetJointParam(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetJointParam(m2ReplayCursor* r, const m2OpPayload* p) { return m2SetJointParamInternal(r->world, p->jointParam.joint, p->jointParam.param, p->jointParam.value); } -static bool ApplyJointUserData(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyJointUserData(m2ReplayCursor* r, const m2OpPayload* p) { m2Joint_SetUserData(r->world, p->jointUserData.joint, p->jointUserData.userData); return true; } -static bool ApplyMotorOffsets(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyMotorOffsets(m2ReplayCursor* r, const m2OpPayload* p) { m2MotorJoint_SetOffsets(r->world, p->motorOffsets.joint, p->motorOffsets.linear, p->motorOffsets.angular); return true; } -static bool ApplyMouseTarget(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyMouseTarget(m2ReplayCursor* r, const m2OpPayload* p) { m2MouseJoint_SetTarget(r->world, p->mouseTarget.joint, p->mouseTarget.target); return true; } -// Particles and fluid volumes. +// Particles and water volumes. // The particle def a create or fill record describes. static m2ParticleDef RecordedParticleDef(m2Pos2 position, m2Vec2 velocity, uint32_t flags) @@ -366,7 +365,7 @@ static m2ParticleDef RecordedParticleDef(m2Pos2 position, m2Vec2 velocity, uint3 return def; } -static bool ApplyCreateParticle(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyCreateParticle(m2ReplayCursor* r, const m2OpPayload* p) { const m2OpCreateParticle* op = &p->createParticle; m2ParticleDef def = RecordedParticleDef(op->position, op->velocity, op->flags); @@ -374,7 +373,7 @@ static bool ApplyCreateParticle(m2ReplayCursor* r, const m2OpPayload* p) return m2CreateParticle(r->world, &def, &id) == m2_success && id.index1 == op->expected.index1; } -static bool ApplyFillParticles(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyFillParticles(m2ReplayCursor* r, const m2OpPayload* p) { const m2OpFillParticles* op = &p->fillParticles; m2ParticleDef def = RecordedParticleDef(op->position, op->velocity, op->flags); @@ -383,74 +382,80 @@ static bool ApplyFillParticles(m2ReplayCursor* r, const m2OpPayload* p) return (result == m2_success || result == m2_errorCapacity) && made == op->expected; } -static bool ApplyDestroyParticle(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyDestroyParticle(m2ReplayCursor* r, const m2OpPayload* p) { return m2DestroyParticle(r->world, p->particleId) == m2_success; } -static bool ApplySetParticleVelocity(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetParticleVelocity(m2ReplayCursor* r, const m2OpPayload* p) { m2Particle_SetVelocity(r->world, p->particleVec.id, p->particleVec.value); return true; } -static bool ApplySetParticleLifetime(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetParticleLifetime(m2ReplayCursor* r, const m2OpPayload* p) { m2Particle_SetLifetime(r->world, p->particleFloat.id, p->particleFloat.value); return true; } -static bool ApplySetParticleUserData(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetParticleUserData(m2ReplayCursor* r, const m2OpPayload* p) { m2Particle_SetUserData(r->world, p->particleUserData.id, p->particleUserData.userData); return true; } -static bool ApplyCreateFluidVolume(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyCreateWaterVolume(m2ReplayCursor* r, const m2OpPayload* p) { - m2FluidVolumeId id = m2_nullFluidVolumeId; - return m2CreateFluidVolume(r->world, &p->createFluidVolume.def, &id) == m2_success && - id.index1 == p->createFluidVolume.expected.index1; + m2WaterVolumeId id = m2_nullWaterVolumeId; + return m2CreateWaterVolume(r->world, &p->createWaterVolume.def, &id) == m2_success && + id.index1 == p->createWaterVolume.expected.index1; } -static bool ApplyDestroyFluidVolume(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyDestroyWaterVolume(m2ReplayCursor* r, const m2OpPayload* p) { - return m2DestroyFluidVolume(r->world, p->fluidVolumeId) == m2_success; + return m2DestroyWaterVolume(r->world, p->waterVolumeId) == m2_success; +} + +M2_NODISCARD static bool ApplySetWaterSurface(m2ReplayCursor* r, const m2OpPayload* p) +{ + m2WaterVolume_SetSurface(r->world, p->waterSurface.id, p->waterSurface.surface); + return true; } -static bool ApplySetFluidSurface(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyWaterUserData(m2ReplayCursor* r, const m2OpPayload* p) { - m2FluidVolume_SetSurface(r->world, p->fluidSurface.id, p->fluidSurface.surface); + m2WaterVolume_SetUserData(r->world, p->waterUserData.id, p->waterUserData.userData); return true; } // World-level ops. -static bool ApplySetGravity(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetGravity(m2ReplayCursor* r, const m2OpPayload* p) { m2World_SetGravity(r->world, p->vec.value); return true; } -static bool ApplySetWind(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplySetWind(m2ReplayCursor* r, const m2OpPayload* p) { m2World_SetWind(r->world, p->setWind.velocity, p->setWind.linearDrag); return true; } -static bool ApplyEnableSleep(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyEnableSleep(m2ReplayCursor* r, const m2OpPayload* p) { m2World_EnableSleep(r->world, p->flag.flag != 0); return true; } -static bool ApplyExplode(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyExplode(m2ReplayCursor* r, const m2OpPayload* p) { m2World_Explode(r->world, &p->explosion); return true; } -static bool ApplyRestore(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyRestore(m2ReplayCursor* r, const m2OpPayload* p) { int64_t bytes = p->restoreSize; if (bytes <= 0 || bytes > r->size - r->offset) @@ -465,7 +470,7 @@ static bool ApplyRestore(m2ReplayCursor* r, const m2OpPayload* p) return true; } -static bool ApplyShatterBody(m2ReplayCursor* r, const m2OpPayload* p) +M2_NODISCARD static bool ApplyShatterBody(m2ReplayCursor* r, const m2OpPayload* p) { const m2OpShatterHeader* op = &p->shatterHeader; int64_t pieceBytes = 0; @@ -486,7 +491,7 @@ static bool ApplyShatterBody(m2ReplayCursor* r, const m2OpPayload* p) m2BodyId first[1] = {m2_nullBodyId}; int32_t made = 0; m2Result shattered = - m2World_ShatterBody(r->world, op->body, pieces, op->pieceCount, first, 1, &made); + m2Body_Shatter(r->world, op->body, pieces, op->pieceCount, first, 1, &made); m2WorldFree(r->world, pieces, (size_t)pieceBytes); return shattered == m2_success && made == op->pieceCount && first[0].index1 == (uint32_t)op->expectedFirst; @@ -505,8 +510,8 @@ static const uint16_t s_bodyBools[] = {M2_AT(m2OpCreateBody, def.motionLocks.lin M2_AT(m2OpCreateBody, def.motionLocks.linearY), M2_AT(m2OpCreateBody, def.motionLocks.angularZ), M2_AT(m2OpCreateBody, def.enableSleep), - M2_AT(m2OpCreateBody, def.isEnabled), - M2_AT(m2OpCreateBody, def.isBullet)}; + M2_AT(m2OpCreateBody, def.enable), + M2_AT(m2OpCreateBody, def.enableBullet)}; static const uint16_t s_shapeBools[] = {M2_AT(m2OpCreateShape, def.isSensor)}; static const uint16_t s_distanceBools[] = { M2_AT(m2OpCreateDistanceJoint, def.enableSpring), @@ -603,10 +608,11 @@ static const m2JournalCommand s_commands[m2_opCount] = { M2_COMMAND(m2_opShatterBody, m2OpShatterHeader, ApplyShatterBody), M2_COMMAND(m2_opSetParticleLifetime, m2OpParticleFloat, ApplySetParticleLifetime), M2_COMMAND(m2_opSetParticleUserData, m2OpParticleUserData, ApplySetParticleUserData), - M2_COMMAND(m2_opCreateFluidVolume, m2OpCreateFluidVolume, ApplyCreateFluidVolume), - M2_COMMAND(m2_opDestroyFluidVolume, m2FluidVolumeId, ApplyDestroyFluidVolume), - M2_COMMAND(m2_opSetFluidSurface, m2OpFluidSurface, ApplySetFluidSurface), + M2_COMMAND(m2_opCreateWaterVolume, m2OpCreateWaterVolume, ApplyCreateWaterVolume), + M2_COMMAND(m2_opDestroyWaterVolume, m2WaterVolumeId, ApplyDestroyWaterVolume), + M2_COMMAND(m2_opSetWaterSurface, m2OpWaterSurface, ApplySetWaterSurface), M2_COMMAND(m2_opSetWind, m2OpSetWind, ApplySetWind), + M2_COMMAND(m2_opWaterUserData, m2OpWaterUserData, ApplyWaterUserData), }; #undef M2_COMMAND diff --git a/third_party/maul2d/src/manifold.c b/third_party/maul2d/src/manifold.c index 149e4ff7e..7c8afb6f4 100644 --- a/third_party/maul2d/src/manifold.c +++ b/third_party/maul2d/src/manifold.c @@ -334,8 +334,8 @@ static m2Manifold ClipToFace(const m2Polygon* ref, const m2Polygon* inc, int32_t // Rounded polygons apart from each other can touch corner to corner, // closer than any clipped point: one point on the line between the two // corners replaces the clip then. -static bool CornerContact(const m2Polygon* la, const m2Polygon* lb, int32_t edgeA, int32_t edgeB, - m2Manifold* manifold) +M2_NODISCARD static bool CornerContact(const m2Polygon* la, const m2Polygon* lb, int32_t edgeA, + int32_t edgeB, m2Manifold* manifold) { int32_t a2 = edgeA + 1 < la->count ? edgeA + 1 : 0; int32_t b2 = edgeB + 1 < lb->count ? edgeB + 1 : 0; @@ -393,7 +393,7 @@ m2Manifold m2CollidePolygons(const m2Polygon* a, const m2Polygon* b, m2RelativeP m2Manifold manifold = ClipToFace(ref, inc, face, edge, flip); if (m2MaxF(fa.separation, fb.separation) > M2_MANIFOLD_TOLERANCE) { - CornerContact(&la, &lb, flip ? edge : face, flip ? face : edge, &manifold); + (void)CornerContact(&la, &lb, flip ? edge : face, flip ? face : edge, &manifold); } // Two points always run along the tangent (the normal turned // counterclockwise), whichever polygon holds the reference face: the diff --git a/third_party/maul2d/src/mass.h b/third_party/maul2d/src/mass.h index 2dd022070..ce18fcec4 100644 --- a/third_party/maul2d/src/mass.h +++ b/third_party/maul2d/src/mass.h @@ -38,7 +38,7 @@ m2BodyMass m2BodyMassFromShapes(uint8_t type, bool fixedRotation, const m2MassSh // inverse of an inertia within [M2_MIN_INERTIA, M2_INERTIA_LIMIT]. // m2Body_SetMassData and the snapshot check share it, so what one accepts // the other does. -bool m2MassInversesValid(float invMass, float invInertia); +M2_NODISCARD bool m2MassInversesValid(float invMass, float invInertia); // Sets a body's inverse mass, inverse inertia and center of mass from its // shapes, in shape-list order, and drops any override. diff --git a/third_party/maul2d/src/math_functions.c b/third_party/maul2d/src/math_functions.c index e7c7cf1a7..f69464225 100644 --- a/third_party/maul2d/src/math_functions.c +++ b/third_party/maul2d/src/math_functions.c @@ -31,14 +31,6 @@ M2_NOINLINE float m2UnwindAngleWork(float radians) return m2WrapAngle(radians); } -float m2UnwindAngle(float radians) -{ - uint64_t env = m2FloatEnvEnter(); - float result = m2UnwindAngleWork(radians); - m2FloatEnvLeave(env); - return result; -} - M2_NOINLINE m2Rot m2MakeRotWork(float radians) { return m2RotOfAngle(radians); @@ -128,7 +120,7 @@ m2Rot m2MulRot(m2Rot q, m2Rot r) return result; } -static M2_NOINLINE bool IsNormalizedRotWorker(m2Rot q) +M2_NODISCARD static M2_NOINLINE bool IsNormalizedRotWorker(m2Rot q) { float mag2 = q.c * q.c + q.s * q.s; float tolerance = 4.0f * 1.19209290e-7f; // 4 * FLT_EPSILON diff --git a/third_party/maul2d/src/math_functions.h b/third_party/maul2d/src/math_functions.h index 18975fbdc..c2e82f343 100644 --- a/third_party/maul2d/src/math_functions.h +++ b/third_party/maul2d/src/math_functions.h @@ -2,9 +2,10 @@ // Copyright (c) 2026 Sirac Ozmen // // The engine's own angle functions, for code already inside the -// floating-point environment: the public m2UnwindAngle, m2MakeRot, -// m2Atan2, m2NormalizeRot and m2MulRot enter it around these, and the -// solver's passes call them directly instead of entering again. +// floating-point environment: the public m2MakeRot, m2Atan2, +// m2NormalizeRot and m2MulRot enter it around these, and the solver's +// passes call them directly instead of entering again. m2UnwindAngleWork +// maps an angle into [-pi, pi] for the joints and the target transform. #ifndef MAUL2D_SRC_MATH_FUNCTIONS_H #define MAUL2D_SRC_MATH_FUNCTIONS_H diff --git a/third_party/maul2d/src/mover.c b/third_party/maul2d/src/mover.c index 674b8356e..f19950ac5 100644 --- a/third_party/maul2d/src/mover.c +++ b/third_party/maul2d/src/mover.c @@ -22,6 +22,68 @@ #include +// Keeps the `capacity` planes of the lowest shape slots offered, as a +// max-heap on the slot while collecting, so which planes a short array +// holds depends only on which shapes touch, never on the order the trees +// yield them. SortPlanes then lays them out ascending. +static void SiftPlaneDown(m2MoverPlane* heap, int32_t size, int32_t i) +{ + for (;;) + { + int32_t largest = i; + int32_t left = 2 * i + 1; + int32_t right = left + 1; + if (left < size && heap[left].shapeId.index1 > heap[largest].shapeId.index1) + { + largest = left; + } + if (right < size && heap[right].shapeId.index1 > heap[largest].shapeId.index1) + { + largest = right; + } + if (largest == i) + { + return; + } + m2MoverPlane swap = heap[i]; + heap[i] = heap[largest]; + heap[largest] = swap; + i = largest; + } +} + +static void OfferPlane(m2MoverPlane* heap, int32_t* size, int32_t capacity, m2MoverPlane plane) +{ + if (*size < capacity) + { + int32_t i = (*size)++; + heap[i] = plane; + while (i > 0 && heap[(i - 1) / 2].shapeId.index1 < heap[i].shapeId.index1) + { + m2MoverPlane swap = heap[i]; + heap[i] = heap[(i - 1) / 2]; + heap[(i - 1) / 2] = swap; + i = (i - 1) / 2; + } + } + else if (capacity > 0 && plane.shapeId.index1 < heap[0].shapeId.index1) + { + heap[0] = plane; + SiftPlaneDown(heap, *size, 0); + } +} + +static void SortPlanes(m2MoverPlane* heap, int32_t size) +{ + for (int32_t end = size - 1; end > 0; --end) + { + m2MoverPlane swap = heap[0]; + heap[0] = heap[end]; + heap[end] = swap; + SiftPlaneDown(heap, end, 0); + } +} + // Planes for a posed capsule: one distance query per nearby shape. A // collar of four linear slops lets a controller see a wall before it // reaches it. @@ -31,10 +93,10 @@ static M2_NOINLINE int32_t WorldCollideMoverWorker(const m2World* world, const m int32_t capacity, m2QueryFilter filter) { m2DistanceProxy moverLocal = m2CapsuleProxy(mover); - if (world == NULL || moverLocal.count == 0 || !m2QueryMotionValid(origin, (m2Vec2){0.0f, 0.0f})) + if (world == NULL || !m2ListOutputValid(results, capacity) || moverLocal.count == 0 || + !m2QueryMotionValid(origin, (m2Vec2){0.0f, 0.0f})) { - m2Refuse(world, m2_errorInvalid); - return 0; + return m2RefuseList(world, results, capacity, sizeof(*results)); } m2ProxyQuery q = m2MakeProxyQuery(&moverLocal, origin, (m2Vec2){0.0f, 0.0f}); @@ -46,6 +108,7 @@ static M2_NOINLINE int32_t WorldCollideMoverWorker(const m2World* world, const m aabb.upperBound.y = q.pose.p.y + (double)(q.boundRadius + collar); int32_t total = 0; + int32_t kept = 0; for (int32_t t = 0; t < M2_TREE_COUNT; ++t) { m2TreeCursor cursor; @@ -92,41 +155,30 @@ static M2_NOINLINE int32_t WorldCollideMoverWorker(const m2World* world, const m m2Transform xf = world->bodies.transforms[body]; m2Vec2 surf = {d.pointA.x + target.radius * normalLocal.x, d.pointA.y + target.radius * normalLocal.y}; - if (results != NULL && total < capacity) + if (results != NULL) { - m2MoverPlane* out = results + total; - out->shapeId.index1 = shapeIndex + 1; - out->shapeId.generation = world->shapes.shapeGenerations[shapeIndex]; - out->normal = (m2Vec2){xf.q.c * normalLocal.x - xf.q.s * normalLocal.y, - xf.q.s * normalLocal.x + xf.q.c * normalLocal.y}; - out->separation = separation; - out->point = (m2Pos2){xf.p.x + (double)(xf.q.c * surf.x - xf.q.s * surf.y), - xf.p.y + (double)(xf.q.s * surf.x + xf.q.c * surf.y)}; + m2MoverPlane plane; + plane.shapeId.index1 = (uint32_t)(shapeIndex + 1); + plane.shapeId.generation = world->shapes.shapeGenerations[shapeIndex]; + plane.normal = (m2Vec2){xf.q.c * normalLocal.x - xf.q.s * normalLocal.y, + xf.q.s * normalLocal.x + xf.q.c * normalLocal.y}; + plane.separation = separation; + plane.point = (m2Pos2){xf.p.x + (double)(xf.q.c * surf.x - xf.q.s * surf.y), + xf.p.y + (double)(xf.q.s * surf.x + xf.q.c * surf.y)}; + OfferPlane(results, &kept, capacity, plane); } total += 1; } } - // Ascending shape order for the filled portion (canonical). - int32_t filled = results != NULL ? (total < capacity ? total : capacity) : 0; - for (int32_t i = 1; i < filled; ++i) - { - m2MoverPlane key = results[i]; - int32_t j = i - 1; - while (j >= 0 && results[j].shapeId.index1 > key.shapeId.index1) - { - results[j + 1] = results[j]; - j -= 1; - } - results[j + 1] = key; - } + SortPlanes(results, kept); // ascending shape order (canonical) return total; } int32_t m2World_CollideMover(const m2World* world, const m2Capsule* mover, m2Transform origin, - m2MoverPlane* results, int32_t capacity, m2QueryFilter filter) + m2QueryFilter filter, m2MoverPlane* planesOut, int32_t capacity) { uint64_t env = m2FloatEnvEnter(); - int32_t result = WorldCollideMoverWorker(world, mover, origin, results, capacity, filter); + int32_t result = WorldCollideMoverWorker(world, mover, origin, planesOut, capacity, filter); m2FloatEnvLeave(env); return result; } @@ -149,8 +201,8 @@ static float Dot(m2Vec2 a, m2Vec2 b) // The point closest to the wish on the planes of set (k of them), // pushed by each: solves the Gram system G lambda = -(s + N w). False // when the set is degenerate or a push would pull. -static bool RestOn(m2Vec2 wish, const m2MoverPlane* planes, const int32_t* set, int32_t k, - m2Vec2* d, uint32_t* pressed) +M2_NODISCARD static bool RestOn(m2Vec2 wish, const m2MoverPlane* planes, const int32_t* set, + int32_t k, m2Vec2* d, uint32_t* pressed) { float lambda[2] = {0.0f, 0.0f}; if (k == 1) @@ -221,7 +273,7 @@ static M2_NOINLINE m2MoverMove SolveMoverWorker(m2Vec2 wish, const m2MoverPlane* { return move; } - count = count < M2_MOVER_PLANES ? count : M2_MOVER_PLANES; + count = count < M2_MAX_MOVER_PLANES ? count : M2_MAX_MOVER_PLANES; Candidate best = {wish, -3.4e38f, 0}; int32_t set[2] = {0, 0}; Consider(wish, planes, count, set, 0, &best); @@ -261,9 +313,9 @@ static M2_NOINLINE m2Vec2 ClipMoverVelocityWorker(m2Vec2 velocity, const m2Mover { return velocity; } - m2MoverPlane touching[M2_MOVER_PLANES]; + m2MoverPlane touching[M2_MAX_MOVER_PLANES]; int32_t n = 0; - for (int32_t i = 0; i < count && i < M2_MOVER_PLANES; ++i) + for (int32_t i = 0; i < count && i < M2_MAX_MOVER_PLANES; ++i) { if ((pressed & (1u << i)) != 0) { diff --git a/third_party/maul2d/src/parallel.c b/third_party/maul2d/src/parallel.c index 1fe8630a1..a51065c3b 100644 --- a/third_party/maul2d/src/parallel.c +++ b/third_party/maul2d/src/parallel.c @@ -12,7 +12,7 @@ // shares. typedef struct TaskCall { - m2TaskFn* fn; + m2TaskFunction* fn; void* context; } TaskCall; @@ -56,7 +56,8 @@ static M2_NOINLINE void Finish(const m2World* world, void* task) m2FloatHostLeave(host); } -void m2RunParallel(m2World* world, m2TaskFn* fn, void* ctx, int32_t itemCount, int32_t minRange) +void m2RunParallel(m2World* world, m2TaskFunction* fn, void* ctx, int32_t itemCount, + int32_t minRange) { if (itemCount <= 0) { diff --git a/third_party/maul2d/src/parallel.h b/third_party/maul2d/src/parallel.h index b316362a9..b384cf743 100644 --- a/third_party/maul2d/src/parallel.h +++ b/third_party/maul2d/src/parallel.h @@ -11,6 +11,7 @@ // Runs fn over [0, itemCount) in ranges of at least minRange: through the // host's task hooks when it installed them, serial in the caller // otherwise. Results never depend on the split. -void m2RunParallel(m2World* world, m2TaskFn* fn, void* ctx, int32_t itemCount, int32_t minRange); +void m2RunParallel(m2World* world, m2TaskFunction* fn, void* ctx, int32_t itemCount, + int32_t minRange); #endif // MAUL2D_SRC_PARALLEL_H diff --git a/third_party/maul2d/src/particle.c b/third_party/maul2d/src/particle.c index 0712e4cad..f7ed2a562 100644 --- a/third_party/maul2d/src/particle.c +++ b/third_party/maul2d/src/particle.c @@ -46,7 +46,9 @@ static m2Result ParticleDefCheck(const m2ParticleDef* def) { return m2_errorInvalid; } - return m2BothChecks(m2CheckValid(m2FinitePos2(def->position)), m2CheckVelocity(def->velocity)); + bool flags = (def->flags & ~M2_PARTICLE_FLAGS) == 0; + return m2BothChecks(m2CheckValid(m2FinitePos2(def->position) && flags), + m2CheckVelocity(def->velocity)); } static M2_NOINLINE m2Result CreateParticleWorker(m2World* world, const m2ParticleDef* def, @@ -71,7 +73,7 @@ static M2_NOINLINE m2Result CreateParticleWorker(m2World* world, const m2Particl { // A full pool is a runtime fact, not misuse: pace emitters // off m2World_GetParticleCount; the counter keeps the score. - world->particles.particlePoolFullCount += 1; + world->particles.particleCreatesRefused += 1; return m2Refuse(world, m2_errorCapacity); } int32_t index = world->particles.particleFreeQueue[world->particles.particleFreeHead]; @@ -241,7 +243,8 @@ static M2_NOINLINE void ParticleSetVelocityWorker(m2World* world, m2ParticleId p int32_t index = ParticleSlot(world, particleId); if (index < 0) { - return; // expired: changes nothing, counts nothing + m2RefuseUncounted(world, m2_errorStale); // expired: changes nothing, counts nothing + return; } if (!m2ValidVelocity(velocity)) { @@ -266,7 +269,7 @@ void m2Particle_SetVelocity(m2World* world, m2ParticleId particleId, m2Vec2 velo m2FloatEnvLeave(env); } -uint32_t m2Particle_GetFlags(const m2World* world, m2ParticleId particleId) +m2ParticleFlags m2Particle_GetFlags(const m2World* world, m2ParticleId particleId) { int32_t index = ParticleSlot(world, particleId); return index >= 0 ? world->particles.particleFlags[index] : 0; @@ -282,7 +285,8 @@ static M2_NOINLINE void ParticleSetLifetimeWorker(m2World* world, m2ParticleId p int32_t index = ParticleSlot(world, particleId); if (index < 0) { - return; // expired: changes nothing, counts nothing + m2RefuseUncounted(world, m2_errorStale); // expired: changes nothing, counts nothing + return; } if (!m2FiniteF(seconds) || seconds < 0.0f) { @@ -330,7 +334,8 @@ void m2Particle_SetUserData(m2World* world, m2ParticleId particleId, uint64_t us int32_t index = ParticleSlot(world, particleId); if (index < 0) { - return; // expired: changes nothing, counts nothing + m2RefuseUncounted(world, m2_errorStale); // expired: changes nothing, counts nothing + return; } if (world->recorder.journalActive != 0) { @@ -354,11 +359,11 @@ int32_t m2World_GetParticleCount(const m2World* world) return world != NULL ? world->particles.particleCount : 0; } -int32_t m2World_GetParticles(const m2World* world, m2ParticleId* ids, int32_t capacity) +int32_t m2World_GetParticles(const m2World* world, m2ParticleId* idsOut, int32_t capacity) { - if (world == NULL) + if (world == NULL || !m2ListOutputValid(idsOut, capacity)) { - return 0; + return m2RefuseList(world, idsOut, capacity, sizeof(*idsOut)); } int32_t total = 0; for (int32_t i = 0; i < world->particles.maxParticleIndex; ++i) @@ -367,9 +372,10 @@ int32_t m2World_GetParticles(const m2World* world, m2ParticleId* ids, int32_t ca { continue; } - if (ids != NULL && total < capacity) + if (idsOut != NULL && total < capacity) { - ids[total] = (m2ParticleId){(uint32_t)(i + 1), world->particles.particleGenerations[i]}; + idsOut[total] = + (m2ParticleId){(uint32_t)(i + 1), world->particles.particleGenerations[i]}; } total += 1; } @@ -389,7 +395,7 @@ typedef struct FillBatch int32_t currRow[M2_MAX_FILL_COLUMNS]; } FillBatch; -static bool InsidePolygon(const m2Polygon* polygon, float x, float y) +M2_NODISCARD static bool InsidePolygon(const m2Polygon* polygon, float x, float y) { for (int32_t i = 0; i < polygon->count; ++i) { @@ -435,7 +441,10 @@ static void AddCellTriads(m2World* world, const FillBatch* batch, int32_t col, i } // Every batch pair inside one diameter becomes a spring that remembers -// its spawn length, in ascending order; a full spring pool truncates. +// its spawn length, in ascending order; a full spring pool truncates. The +// batch comes row by row from the bottom, so once a later particle lies a +// diameter or more above, every one after it does too, and no pair from +// there on can be a spring. static void AddFillSprings(m2World* world, const FillBatch* batch) { m2Particles* p = &world->particles; @@ -448,6 +457,10 @@ static void AddFillSprings(m2World* world, const FillBatch* batch) int32_t b = batch->emitted[j]; float dx = (float)(p->particlePositions[b].x - p->particlePositions[a].x); float dy = (float)(p->particlePositions[b].y - p->particlePositions[a].y); + if (dy >= diameter) + { + break; + } float distSq = dx * dx + dy * dy; if (distSq >= diameter * diameter || distSq <= 0.0f || p->particleSpringCount >= p->particleSpringCapacity) @@ -464,8 +477,8 @@ static void AddFillSprings(m2World* world, const FillBatch* batch) } // Emits one lattice row; false once the pool is full. -static bool FillRow(m2World* world, FillBatch* batch, const m2Polygon* polygon, float minX, - float maxX, float y, const m2OpFillParticles* op) +M2_NODISCARD static bool FillRow(m2World* world, FillBatch* batch, const m2Polygon* polygon, + float minX, float maxX, float y, const m2OpFillParticles* op) { for (int32_t i = 0; i < M2_MAX_FILL_COLUMNS; ++i) { @@ -517,18 +530,60 @@ static void PolygonBounds(const m2Polygon* polygon, m2Vec2* lower, m2Vec2* upper } } +// Fills the region's lattice row by row from the bottom of its bounds and +// says whether the pool ran out. The whole fill is one journal op with the +// inner creates suppressed: replay rebuilds the springs and triads from +// the op. +M2_NODISCARD static bool FillLattice(m2World* world, FillBatch* batch, const m2Polygon* region, + m2Vec2 lower, m2Vec2 upper, const m2OpFillParticles* op) +{ + uint8_t journalWas = world->recorder.journalActive; + world->recorder.journalActive = 0; + batch->emitted = (int32_t*)world->particles.particleProxies; + batch->count = 0; + for (int32_t i = 0; i < M2_MAX_FILL_COLUMNS; ++i) + { + batch->prevRow[i] = -1; + } + // The row coordinate accumulates stride by stride, since the sum is + // part of the result. The fill ends where a row no longer moves it, or + // where the pool runs out. + bool full = false; + bool open = true; + float y = lower.y + 0.5f * batch->stride; + while (y < upper.y && open) + { + full = !FillRow(world, batch, region, lower.x, upper.x, y, op); + float next = y + batch->stride; + open = !full && next > y; + y = next; + } + if ((op->flags & m2_springParticle) != 0) + { + AddFillSprings(world, batch); + } + world->recorder.journalActive = journalWas; + return full; +} + // Fill a convex polygon with particles at 0.75 diameters, row-major from // the bottom-left of its bounds: a pool in one call. static M2_NOINLINE m2Result CreateParticlesInPolygonWorker(m2World* world, const m2ParticleDef* def, const m2Polygon* polygon, int32_t* countOut) { + // A busy refusal leaves countOut, which host code the world is running + // may be filling; every other failure clears it. + m2Result writer = m2CheckWriter(world); + if (writer == m2_errorBusy) + { + return writer; + } if (countOut == NULL) { return m2Refuse(world, m2_errorInvalid); } *countOut = 0; - m2Result writer = m2CheckWriter(world); if (writer != m2_success) { return writer; @@ -543,7 +598,7 @@ static M2_NOINLINE m2Result CreateParticlesInPolygonWorker(m2World* world, const if (world->particles.particleCapacity == 0) { // A pool of 0 is full; stop before the unchecked radius sizes a lattice. - world->particles.particlePoolFullCount += 1; + world->particles.particleCreatesRefused += 1; return m2Refuse(world, m2_errorCapacity); } m2Polygon region = m2CanonicalPolygon(polygon); @@ -569,34 +624,7 @@ static M2_NOINLINE m2Result CreateParticlesInPolygonWorker(m2World* world, const op.velocity = def->velocity; op.flags = def->flags; - // The whole fill is one journal op with the inner creates suppressed: - // replay rebuilds the springs and triads from the op. - uint8_t journalWas = world->recorder.journalActive; - world->recorder.journalActive = 0; - batch.emitted = (int32_t*)world->particles.particleProxies; - batch.count = 0; - for (int32_t i = 0; i < M2_MAX_FILL_COLUMNS; ++i) - { - batch.prevRow[i] = -1; - } - // The row coordinate accumulates stride by stride, since the sum is - // part of the result. The fill ends where a row no longer moves it, or - // where the pool runs out. - bool full = false; - bool open = true; - float y = lower.y + 0.5f * batch.stride; - while (y < upper.y && open) - { - full = !FillRow(world, &batch, ®ion, lower.x, upper.x, y, &op); - float next = y + batch.stride; - open = !full && next > y; - y = next; - } - if ((def->flags & m2_springParticle) != 0) - { - AddFillSprings(world, &batch); - } - world->recorder.journalActive = journalWas; + bool full = FillLattice(world, &batch, ®ion, lower, upper, &op); op.expected = batch.count; m2JournalRecord(world, m2_opFillParticles, &op, (int32_t)sizeof(op)); *countOut = batch.count; @@ -622,10 +650,9 @@ static M2_NOINLINE int32_t WorldOverlapParticlesAabbWorker(const m2World* world, { m2Pos2 lower = box.lowerBound; m2Pos2 upper = box.upperBound; - if (world == NULL || !m2FinitePos2(lower) || !m2FinitePos2(upper)) + if (world == NULL || !m2ListOutputValid(ids, capacity) || !m2ValidWorldAabb(box)) { - m2Refuse(world, m2_errorInvalid); - return 0; + return m2RefuseList(world, ids, capacity, sizeof(*ids)); } int32_t total = 0; for (int32_t i = 0; i < world->particles.maxParticleIndex; ++i) @@ -648,11 +675,11 @@ static M2_NOINLINE int32_t WorldOverlapParticlesAabbWorker(const m2World* world, return total; } -int32_t m2World_OverlapParticlesAabb(const m2World* world, m2WorldAabb box, m2ParticleId* ids, +int32_t m2World_OverlapParticlesAabb(const m2World* world, m2WorldAabb box, m2ParticleId* idsOut, int32_t capacity) { uint64_t env = m2FloatEnvEnter(); - int32_t result = WorldOverlapParticlesAabbWorker(world, box, ids, capacity); + int32_t result = WorldOverlapParticlesAabbWorker(world, box, idsOut, capacity); m2FloatEnvLeave(env); return result; } diff --git a/third_party/maul2d/src/particle_contacts.c b/third_party/maul2d/src/particle_contacts.c index 3103e98fb..55087dab3 100644 --- a/third_party/maul2d/src/particle_contacts.c +++ b/third_party/maul2d/src/particle_contacts.c @@ -8,11 +8,14 @@ // that normal. Sensors never touch particles, and a one-way chain link // ignores particles on its ghost side. // -// Each particle's candidates come from the broadphase trees and are -// taken in ascending shape order, so the contacts are canonical: by -// particle, then by shape. Large pools stage the particles in parallel -// ranges; the gather that follows runs serially in particle order, so -// the worker count never changes a bit. +// Each particle tests every non-sensor shape whose box overlaps its own, +// as the broadphase trees yield them, and keeps the touching shapes with +// the lowest slots; every touch past them counts as a drop. What it keeps +// depends only on which shapes touch, not on the order the trees yield +// them, so the contacts are canonical: by particle, then by shape. Large +// pools stage the particles in parallel ranges; the gather that follows +// runs serially in particle order, so the worker count never changes a +// bit. #include "body.h" #include "distance.h" @@ -27,7 +30,6 @@ #include #include -#define M2_PARTICLE_CANDIDATES 32 #define M2_CONTACTS_PER_PARTICLE 4 // Staging a pool this large pays for the fork and join. @@ -38,27 +40,6 @@ static m2Vec2 Rotate(m2Rot q, m2Vec2 v) return (m2Vec2){q.c * v.x - q.s * v.y, q.s * v.x + q.c * v.y}; } -// The non-sensor shapes whose boxes overlap box, ascending: the lowest -// M2_PARTICLE_CANDIDATES slots when more overlap. Returns the count. -static int32_t Candidates(const m2World* world, m2Aabb box, int32_t* out) -{ - int32_t count = 0; - for (int32_t t = 0; t < M2_TREE_COUNT; ++t) - { - m2TreeCursor cursor; - m2TreeBeginQuery(&cursor, &world->broadphase.trees[t], world->broadphase.treeNodes[t], box); - int32_t shape; - while (m2TreeNextQuery(&cursor, &shape)) - { - if (world->shapes.shapeSensor[shape] == 0) - { - m2TreeKeepLowest(out, &count, M2_PARTICLE_CANDIDATES, shape); - } - } - } - return count; -} - // A staged contact slot. typedef struct Touch { @@ -68,8 +49,8 @@ typedef struct Touch } Touch; // The contact between particle p and shape, if any. -static bool TouchShape(const m2World* world, m2Pos2 p, int32_t shape, float h, - float particleInvMass, Touch* out) +M2_NODISCARD static bool TouchShape(const m2World* world, m2Pos2 p, int32_t shape, float h, + float particleInvMass, Touch* out) { int32_t body = world->shapes.shapeBody[shape]; m2Transform xf = world->bodies.transforms[body]; @@ -116,6 +97,61 @@ static bool TouchShape(const m2World* world, m2Pos2 p, int32_t shape, float h, return true; } +// A particle's kept touches, ascending by shape slot. +typedef struct Kept +{ + int32_t count; + int32_t drops; + int32_t shapes[M2_CONTACTS_PER_PARTICLE]; + Touch touches[M2_CONTACTS_PER_PARTICLE]; +} Kept; + +// Keeps a touch if its shape is among the lowest slots so far; the touch +// left out, this one or the highest kept, counts as a drop. +static void KeepTouch(Kept* kept, int32_t shape, Touch touch) +{ + int32_t n = kept->count; + if (n == M2_CONTACTS_PER_PARTICLE) + { + kept->drops += 1; + if (shape > kept->shapes[n - 1]) + { + return; + } + n -= 1; // the highest makes room + } + int32_t i = n; + for (; i > 0 && kept->shapes[i - 1] > shape; --i) + { + kept->shapes[i] = kept->shapes[i - 1]; + kept->touches[i] = kept->touches[i - 1]; + } + kept->shapes[i] = shape; + kept->touches[i] = touch; + kept->count = n + 1; +} + +// Every non-sensor shape in the box that touches the particle at. +static void TouchShapes(const m2World* world, m2Pos2 at, float h, float particleInvMass, Kept* kept) +{ + m2Aabb box = {{at.x - (double)h, at.y - (double)h}, {at.x + (double)h, at.y + (double)h}}; + for (int32_t t = 0; t < M2_TREE_COUNT; ++t) + { + m2TreeCursor cursor; + m2TreeBeginQuery(&cursor, &world->broadphase.trees[t], world->broadphase.treeNodes[t], box); + int32_t shape; + while (m2TreeNextQuery(&cursor, &shape)) + { + Touch touch; + if (world->shapes.shapeSensor[shape] == 0 && + TouchShape(world, at, shape, h, particleInvMass, &touch)) + { + KeepTouch(kept, shape, touch); + } + } + } +} + static void StageRange(int32_t begin, int32_t end, void* context) { m2World* world = (m2World*)context; @@ -134,29 +170,16 @@ static void StageRange(int32_t begin, int32_t end, void* context) { continue; } - m2Pos2 at = p->particlePositions[i]; - m2Aabb box = {{at.x - (double)h, at.y - (double)h}, {at.x + (double)h, at.y + (double)h}}; - int32_t shapes[M2_PARTICLE_CANDIDATES]; - int32_t count = Candidates(world, box, shapes); - int32_t kept = 0; - for (int32_t c = 0; c < count; ++c) + Kept kept = {0}; + TouchShapes(world, p->particlePositions[i], h, particleInvMass, &kept); + p->particleBodyStageDrops[i] = kept.drops; + for (int32_t k = 0; k < kept.count; ++k) { - Touch touch; - if (!TouchShape(world, at, shapes[c], h, particleInvMass, &touch)) - { - continue; - } - if (kept == M2_CONTACTS_PER_PARTICLE) - { - p->particleBodyStageDrops[i] += 1; - continue; - } - int32_t slot = i * M2_CONTACTS_PER_PARTICLE + kept; - p->particleBodyStageBody[slot] = world->shapes.shapeBody[shapes[c]]; - p->particleBodyStageWeight[slot] = touch.weight; - p->particleBodyStageNormal[slot] = touch.normal; - p->particleBodyStageMass[slot] = touch.mass; - kept += 1; + int32_t slot = i * M2_CONTACTS_PER_PARTICLE + k; + p->particleBodyStageBody[slot] = world->shapes.shapeBody[kept.shapes[k]]; + p->particleBodyStageWeight[slot] = kept.touches[k].weight; + p->particleBodyStageNormal[slot] = kept.touches[k].normal; + p->particleBodyStageMass[slot] = kept.touches[k].mass; } } } @@ -167,7 +190,7 @@ static void Gather(m2World* world) m2Particles* p = &world->particles; for (int32_t i = 0; i < p->maxParticleIndex; ++i) { - p->particleBodyOverflow += p->particleBodyStageDrops[i]; + m2CountDrops(&p->particleBodyContactsDropped, p->particleBodyStageDrops[i]); for (int32_t k = 0; k < M2_CONTACTS_PER_PARTICLE; ++k) { int32_t slot = i * M2_CONTACTS_PER_PARTICLE + k; @@ -178,7 +201,7 @@ static void Gather(m2World* world) } if (p->particleBodyCount >= p->particleBodyCapacity) { - p->particleBodyOverflow += 1; + m2CountDrops(&p->particleBodyContactsDropped, 1); continue; } int32_t out = p->particleBodyCount; @@ -196,7 +219,7 @@ static void Gather(m2World* world) void m2UpdateParticleContacts(m2World* world) { world->particles.particleBodyCount = 0; - world->particles.particleBodyOverflow = 0; + world->particles.particleBodyContactsDropped = 0; if (world->particles.particleCount >= M2_FLUID_THREAD_MIN) { m2RunParallel(world, StageRange, world, world->particles.maxParticleIndex, 64); diff --git a/third_party/maul2d/src/particle_pairs.c b/third_party/maul2d/src/particle_pairs.c index f6768256d..8c23d63ad 100644 --- a/third_party/maul2d/src/particle_pairs.c +++ b/third_party/maul2d/src/particle_pairs.c @@ -143,7 +143,7 @@ static void AddPair(m2World* world, int32_t a, int32_t b, float diameter) } if (p->particlePairCount >= p->particlePairCapacity) { - p->particlePairOverflow += 1; + m2CountDrops(&p->particlePairsDropped, 1); return; } int32_t n = p->particlePairCount; @@ -194,7 +194,7 @@ void m2UpdateParticlePairs(m2World* world) { m2Particles* p = &world->particles; p->particlePairCount = 0; - p->particlePairOverflow = 0; + p->particlePairsDropped = 0; p->particleFlagsUnion = 0; if (p->particleCount == 0) { diff --git a/third_party/maul2d/src/proxies.h b/third_party/maul2d/src/proxies.h index 2379ef950..36ddc89b7 100644 --- a/third_party/maul2d/src/proxies.h +++ b/third_party/maul2d/src/proxies.h @@ -13,7 +13,7 @@ m2Aabb m2ShapeTightAabb(const m2World* world, int32_t shapeIndex); int32_t m2ShapeTreeIndex(const m2World* world, int32_t shapeIndex); // Refits a treed shape's fat box around its tight box at its body's pose // when the tight box has left it. Returns whether the box moved. -bool m2RefitProxy(m2World* world, int32_t shapeIndex); +M2_NODISCARD bool m2RefitProxy(m2World* world, int32_t shapeIndex); // Refits every treed shape of a body and marks the ones that moved. void m2RefitBodyProxies(m2World* world, int32_t bodyIndex); void m2PushMoved(m2World* world, int32_t shapeIndex); diff --git a/third_party/maul2d/src/query.c b/third_party/maul2d/src/query.c index 32a562ae1..5f8a1dee8 100644 --- a/third_party/maul2d/src/query.c +++ b/third_party/maul2d/src/query.c @@ -67,7 +67,7 @@ typedef struct m2RayState // either box axis or the segment's normal separates them (the separating // axis test for a segment and a box). Everything runs in f64, so worlds // far from the origin cull exactly. -static bool RayMissesNode(const m2RayState* ray, m2Aabb box) +M2_NODISCARD static bool RayMissesNode(const m2RayState* ray, m2Aabb box) { double ax = ray->origin.x; double ay = ray->origin.y; @@ -185,12 +185,13 @@ static m2QueryHit CastRayClosest(const m2World* world, m2Pos2 origin, m2Vec2 tra return result; } -static M2_NOINLINE bool WorldCastRayClosestWorker(const m2World* world, m2Pos2 origin, - m2Vec2 translation, m2QueryFilter filter, - m2CastHit* hitOut) +M2_NODISCARD static M2_NOINLINE bool WorldCastRayClosestWorker(const m2World* world, m2Pos2 origin, + m2Vec2 translation, + m2QueryFilter filter, + m2CastHit* hitOut) { - return hitOut != NULL ? m2DeliverHit(CastRayClosest(world, origin, translation, filter), hitOut) - : m2RefuseCast(world); + return m2ClearHit(world, hitOut) && + m2DeliverHit(CastRayClosest(world, origin, translation, filter), hitOut); } bool m2World_CastRayClosest(const m2World* world, m2Pos2 origin, m2Vec2 translation, @@ -287,11 +288,9 @@ static M2_NOINLINE int32_t WorldOverlapAabbWorker(const m2World* world, m2WorldA { m2Pos2 lower = box.lowerBound; m2Pos2 upper = box.upperBound; - if (world == NULL || !m2FinitePos2(lower) || !m2FinitePos2(upper) || upper.x < lower.x || - upper.y < lower.y) + if (world == NULL || !m2ListOutputValid(results, capacity) || !m2ValidWorldAabb(box)) { - m2Refuse(world, m2_errorInvalid); - return 0; + return m2RefuseList(world, results, capacity, sizeof(*results)); } m2Aabb aabb = {lower, upper}; @@ -323,11 +322,11 @@ static M2_NOINLINE int32_t WorldOverlapAabbWorker(const m2World* world, m2WorldA return FinishSelection(&sel); } -int32_t m2World_OverlapAabb(const m2World* world, m2WorldAabb box, m2ShapeId* results, - int32_t capacity, m2QueryFilter filter) +int32_t m2World_OverlapAabb(const m2World* world, m2WorldAabb box, m2QueryFilter filter, + m2ShapeId* idsOut, int32_t capacity) { uint64_t env = m2FloatEnvEnter(); - int32_t result = WorldOverlapAabbWorker(world, box, results, capacity, filter); + int32_t result = WorldOverlapAabbWorker(world, box, idsOut, capacity, filter); m2FloatEnvLeave(env); return result; } @@ -442,10 +441,10 @@ static int32_t OverlapProxy(const m2World* world, const m2DistanceProxy* castLoc m2Transform pose, m2ShapeId* ids, int32_t capacity, m2QueryFilter filter) { - if (world == NULL || castLocal->count == 0 || !m2QueryMotionValid(pose, (m2Vec2){0.0f, 0.0f})) + if (world == NULL || !m2ListOutputValid(ids, capacity) || castLocal->count == 0 || + !m2QueryMotionValid(pose, (m2Vec2){0.0f, 0.0f})) { - m2Refuse(world, m2_errorInvalid); - return 0; + return m2RefuseList(world, ids, capacity, sizeof(*ids)); } m2ProxyQuery q = m2MakeProxyQuery(castLocal, pose, (m2Vec2){0.0f, 0.0f}); m2Aabb aabb; @@ -500,13 +499,12 @@ static m2QueryHit CastCircleClosest(const m2World* world, const m2Circle* circle return CastProxyClosest(world, &p, origin, translation, filter); } -static M2_NOINLINE bool WorldCastCircleClosestWorker(const m2World* world, const m2Circle* circle, - m2Transform origin, m2Vec2 translation, - m2QueryFilter filter, m2CastHit* hitOut) +M2_NODISCARD static M2_NOINLINE bool +WorldCastCircleClosestWorker(const m2World* world, const m2Circle* circle, m2Transform origin, + m2Vec2 translation, m2QueryFilter filter, m2CastHit* hitOut) { - return hitOut != NULL - ? m2DeliverHit(CastCircleClosest(world, circle, origin, translation, filter), hitOut) - : m2RefuseCast(world); + return m2ClearHit(world, hitOut) && + m2DeliverHit(CastCircleClosest(world, circle, origin, translation, filter), hitOut); } bool m2World_CastCircleClosest(const m2World* world, const m2Circle* circle, m2Transform origin, @@ -525,15 +523,12 @@ static m2QueryHit CastCapsuleClosest(const m2World* world, const m2Capsule* caps return CastProxyClosest(world, &p, origin, translation, filter); } -static M2_NOINLINE bool WorldCastCapsuleClosestWorker(const m2World* world, - const m2Capsule* capsule, m2Transform origin, - m2Vec2 translation, m2QueryFilter filter, - m2CastHit* hitOut) +M2_NODISCARD static M2_NOINLINE bool +WorldCastCapsuleClosestWorker(const m2World* world, const m2Capsule* capsule, m2Transform origin, + m2Vec2 translation, m2QueryFilter filter, m2CastHit* hitOut) { - return hitOut != NULL - ? m2DeliverHit(CastCapsuleClosest(world, capsule, origin, translation, filter), - hitOut) - : m2RefuseCast(world); + return m2ClearHit(world, hitOut) && + m2DeliverHit(CastCapsuleClosest(world, capsule, origin, translation, filter), hitOut); } bool m2World_CastCapsuleClosest(const m2World* world, const m2Capsule* capsule, m2Transform origin, @@ -553,15 +548,12 @@ static m2QueryHit CastPolygonClosest(const m2World* world, const m2Polygon* poly return CastProxyClosest(world, &p, origin, translation, filter); } -static M2_NOINLINE bool WorldCastPolygonClosestWorker(const m2World* world, - const m2Polygon* polygon, m2Transform origin, - m2Vec2 translation, m2QueryFilter filter, - m2CastHit* hitOut) +M2_NODISCARD static M2_NOINLINE bool +WorldCastPolygonClosestWorker(const m2World* world, const m2Polygon* polygon, m2Transform origin, + m2Vec2 translation, m2QueryFilter filter, m2CastHit* hitOut) { - return hitOut != NULL - ? m2DeliverHit(CastPolygonClosest(world, polygon, origin, translation, filter), - hitOut) - : m2RefuseCast(world); + return m2ClearHit(world, hitOut) && + m2DeliverHit(CastPolygonClosest(world, polygon, origin, translation, filter), hitOut); } bool m2World_CastPolygonClosest(const m2World* world, const m2Polygon* polygon, m2Transform origin, @@ -583,10 +575,10 @@ static M2_NOINLINE int32_t WorldOverlapCircleWorker(const m2World* world, const } int32_t m2World_OverlapCircle(const m2World* world, const m2Circle* circle, m2Transform origin, - m2ShapeId* ids, int32_t capacity, m2QueryFilter filter) + m2QueryFilter filter, m2ShapeId* idsOut, int32_t capacity) { uint64_t env = m2FloatEnvEnter(); - int32_t result = WorldOverlapCircleWorker(world, circle, origin, ids, capacity, filter); + int32_t result = WorldOverlapCircleWorker(world, circle, origin, idsOut, capacity, filter); m2FloatEnvLeave(env); return result; } @@ -600,10 +592,10 @@ static M2_NOINLINE int32_t WorldOverlapCapsuleWorker(const m2World* world, const } int32_t m2World_OverlapCapsule(const m2World* world, const m2Capsule* capsule, m2Transform origin, - m2ShapeId* ids, int32_t capacity, m2QueryFilter filter) + m2QueryFilter filter, m2ShapeId* idsOut, int32_t capacity) { uint64_t env = m2FloatEnvEnter(); - int32_t result = WorldOverlapCapsuleWorker(world, capsule, origin, ids, capacity, filter); + int32_t result = WorldOverlapCapsuleWorker(world, capsule, origin, idsOut, capacity, filter); m2FloatEnvLeave(env); return result; } @@ -617,10 +609,10 @@ static M2_NOINLINE int32_t WorldOverlapPolygonWorker(const m2World* world, const } int32_t m2World_OverlapPolygon(const m2World* world, const m2Polygon* polygon, m2Transform origin, - m2ShapeId* ids, int32_t capacity, m2QueryFilter filter) + m2QueryFilter filter, m2ShapeId* idsOut, int32_t capacity) { uint64_t env = m2FloatEnvEnter(); - int32_t result = WorldOverlapPolygonWorker(world, polygon, origin, ids, capacity, filter); + int32_t result = WorldOverlapPolygonWorker(world, polygon, origin, idsOut, capacity, filter); m2FloatEnvLeave(env); return result; } @@ -679,11 +671,12 @@ static m2QueryHit ShapeCastRay(const m2World* world, m2ShapeId shapeId, m2Pos2 o return result; } -static M2_NOINLINE bool ShapeCastRayWorker(const m2World* world, m2ShapeId shapeId, m2Pos2 origin, - m2Vec2 translation, m2CastHit* hitOut) +M2_NODISCARD static M2_NOINLINE bool ShapeCastRayWorker(const m2World* world, m2ShapeId shapeId, + m2Pos2 origin, m2Vec2 translation, + m2CastHit* hitOut) { - return hitOut != NULL ? m2DeliverHit(ShapeCastRay(world, shapeId, origin, translation), hitOut) - : m2RefuseCast(world); + return m2ClearHit(world, hitOut) && + m2DeliverHit(ShapeCastRay(world, shapeId, origin, translation), hitOut); } bool m2Shape_CastRay(const m2World* world, m2ShapeId shapeId, m2Pos2 origin, m2Vec2 translation, @@ -729,8 +722,8 @@ static int32_t InsertHitSorted(m2CastHit* hits, int32_t kept, int32_t capacity, // One tree leaf of m2World_CastRayAll: false when the shape is dead, // filtered out or missed, else the hit in world space. -static bool RayHitOnShape(const m2World* world, int32_t shapeIndex, m2Pos2 origin, - m2Vec2 translation, m2QueryFilter filter, m2CastHit* out) +M2_NODISCARD static bool RayHitOnShape(const m2World* world, int32_t shapeIndex, m2Pos2 origin, + m2Vec2 translation, m2QueryFilter filter, m2CastHit* out) { if (world->shapes.shapeAlive[shapeIndex] == 0 || !m2QueryShouldSee(world, shapeIndex, filter)) { @@ -755,10 +748,10 @@ static M2_NOINLINE int32_t WorldCastRayAllWorker(const m2World* world, m2Pos2 or m2Vec2 translation, m2CastHit* hits, int32_t capacity, m2QueryFilter filter) { - if (world == NULL || !m2FinitePos2(origin) || !m2FiniteVec2(translation)) + if (world == NULL || !m2ListOutputValid(hits, capacity) || !m2FinitePos2(origin) || + !m2FiniteVec2(translation)) { - m2Refuse(world, m2_errorInvalid); - return 0; + return m2RefuseList(world, hits, capacity, sizeof(*hits)); } int32_t cap = hits != NULL && capacity > 0 ? capacity : 0; int32_t kept = 0; @@ -813,19 +806,20 @@ static M2_NOINLINE int32_t WorldCastRayAllWorker(const m2World* world, m2Pos2 or return total; } -int32_t m2World_CastRayAll(const m2World* world, m2Pos2 origin, m2Vec2 translation, m2CastHit* hits, - int32_t capacity, m2QueryFilter filter) +int32_t m2World_CastRayAll(const m2World* world, m2Pos2 origin, m2Vec2 translation, + m2QueryFilter filter, m2CastHit* hitsOut, int32_t capacity) { uint64_t env = m2FloatEnvEnter(); - int32_t result = WorldCastRayAllWorker(world, origin, translation, hits, capacity, filter); + int32_t result = WorldCastRayAllWorker(world, origin, translation, hitsOut, capacity, filter); m2FloatEnvLeave(env); return result; } // One candidate of CastProxyAll: false when the shape is dead, filtered // out, behind a chain's ghost side or missed, else the hit in world space. -static bool ProxyHitOnShape(const m2World* world, int32_t shapeIndex, const m2ProxyQuery* q, - m2Transform pose, m2QueryFilter filter, m2CastHit* out) +M2_NODISCARD static bool ProxyHitOnShape(const m2World* world, int32_t shapeIndex, + const m2ProxyQuery* q, m2Transform pose, + m2QueryFilter filter, m2CastHit* out) { if (world->shapes.shapeAlive[shapeIndex] == 0 || !m2QueryShouldSee(world, shapeIndex, filter)) { @@ -870,10 +864,10 @@ static int32_t CastProxyAll(const m2World* world, const m2DistanceProxy* castLoc m2Transform pose, m2Vec2 translation, m2CastHit* hits, int32_t capacity, m2QueryFilter filter) { - if (world == NULL || castLocal->count == 0 || !m2QueryMotionValid(pose, translation)) + if (world == NULL || !m2ListOutputValid(hits, capacity) || castLocal->count == 0 || + !m2QueryMotionValid(pose, translation)) { - m2Refuse(world, m2_errorInvalid); - return 0; + return m2RefuseList(world, hits, capacity, sizeof(*hits)); } m2ProxyQuery q = m2MakeProxyQuery(castLocal, pose, translation); @@ -926,12 +920,12 @@ static M2_NOINLINE int32_t WorldCastCircleAllWorker(const m2World* world, const } int32_t m2World_CastCircleAll(const m2World* world, const m2Circle* circle, m2Transform origin, - m2Vec2 translation, m2CastHit* hits, int32_t capacity, - m2QueryFilter filter) + m2Vec2 translation, m2QueryFilter filter, m2CastHit* hitsOut, + int32_t capacity) { uint64_t env = m2FloatEnvEnter(); int32_t result = - WorldCastCircleAllWorker(world, circle, origin, translation, hits, capacity, filter); + WorldCastCircleAllWorker(world, circle, origin, translation, hitsOut, capacity, filter); m2FloatEnvLeave(env); return result; } @@ -946,12 +940,12 @@ static M2_NOINLINE int32_t WorldCastCapsuleAllWorker(const m2World* world, const } int32_t m2World_CastCapsuleAll(const m2World* world, const m2Capsule* capsule, m2Transform origin, - m2Vec2 translation, m2CastHit* hits, int32_t capacity, - m2QueryFilter filter) + m2Vec2 translation, m2QueryFilter filter, m2CastHit* hitsOut, + int32_t capacity) { uint64_t env = m2FloatEnvEnter(); int32_t result = - WorldCastCapsuleAllWorker(world, capsule, origin, translation, hits, capacity, filter); + WorldCastCapsuleAllWorker(world, capsule, origin, translation, hitsOut, capacity, filter); m2FloatEnvLeave(env); return result; } @@ -966,12 +960,12 @@ static M2_NOINLINE int32_t WorldCastPolygonAllWorker(const m2World* world, const } int32_t m2World_CastPolygonAll(const m2World* world, const m2Polygon* polygon, m2Transform origin, - m2Vec2 translation, m2CastHit* hits, int32_t capacity, - m2QueryFilter filter) + m2Vec2 translation, m2QueryFilter filter, m2CastHit* hitsOut, + int32_t capacity) { uint64_t env = m2FloatEnvEnter(); int32_t result = - WorldCastPolygonAllWorker(world, polygon, origin, translation, hits, capacity, filter); + WorldCastPolygonAllWorker(world, polygon, origin, translation, hitsOut, capacity, filter); m2FloatEnvLeave(env); return result; } diff --git a/third_party/maul2d/src/query.h b/third_party/maul2d/src/query.h index 61453affd..9775250c4 100644 --- a/third_party/maul2d/src/query.h +++ b/third_party/maul2d/src/query.h @@ -23,7 +23,8 @@ struct m2CastHitInternal m2RayCastShapeIndex(const m2World* world, int32_t shape m2Pos2 origin, m2Vec2 translation, float maxFraction); // The layer filter every query applies to a candidate shape. -static inline bool m2QueryShouldSee(const m2World* world, int32_t shapeIndex, m2QueryFilter filter) +M2_NODISCARD static inline bool m2QueryShouldSee(const m2World* world, int32_t shapeIndex, + m2QueryFilter filter) { return (filter.categoryBits & world->shapes.shapeMask[shapeIndex]) != 0 && (world->shapes.shapeCategory[shapeIndex] & filter.maskBits) != 0; @@ -40,16 +41,26 @@ typedef struct m2QueryHit bool hit; } m2QueryHit; -// A closest cast with no place for its hit: one misuse, and no hit. -static inline bool m2RefuseCast(const m2World* world) +// Zeroes a closest cast's hit before the cast checks anything, so a miss, +// a refusal and a null world all leave zeros. A null hitOut has no place +// for the hit: one misuse, and no hit. +M2_NODISCARD static inline bool m2ClearHit(const m2World* world, m2CastHit* hitOut) { - m2Refuse(world, m2_errorInvalid); - return false; + if (hitOut == NULL) + { + m2Refuse(world, m2_errorInvalid); + return false; + } + hitOut->shapeId = m2_nullShapeId; + hitOut->point = (m2Pos2){0.0, 0.0}; + hitOut->normal = (m2Vec2){0.0f, 0.0f}; + hitOut->fraction = 0.0f; + return true; } -// Writes a closest cast's hit, when it has one; leaves hitOut as it was -// otherwise. -static inline bool m2DeliverHit(m2QueryHit result, m2CastHit* hitOut) +// Writes a closest cast's hit, when it has one, over the zeros m2ClearHit +// left. +M2_NODISCARD static inline bool m2DeliverHit(m2QueryHit result, m2CastHit* hitOut) { if (result.hit) { @@ -85,12 +96,12 @@ typedef struct m2ProxyQuery m2DistanceProxy m2CircleProxy(const m2Circle* circle); m2DistanceProxy m2CapsuleProxy(const m2Capsule* capsule); m2DistanceProxy m2PolygonProxy(const m2Polygon* polygon); -bool m2QueryMotionValid(m2Transform pose, m2Vec2 translation); +M2_NODISCARD bool m2QueryMotionValid(m2Transform pose, m2Vec2 translation); m2ProxyQuery m2MakeProxyQuery(const m2DistanceProxy* castLocal, m2Transform pose, m2Vec2 translation); void m2ProxiesInBodyFrame(const m2World* world, int32_t shapeIndex, const m2ProxyQuery* q, m2DistanceProxy* target, m2DistanceProxy* cast, m2Vec2* translationLocal, m2Vec2* poseOriginLocal); -bool m2ChainGhostSide(const m2World* world, int32_t shapeIndex, m2Vec2 startLocal); +M2_NODISCARD bool m2ChainGhostSide(const m2World* world, int32_t shapeIndex, m2Vec2 startLocal); #endif // MAUL2D_SRC_QUERY_H diff --git a/third_party/maul2d/src/ray_cast.c b/third_party/maul2d/src/ray_cast.c index d0b3029e9..e56331d96 100644 --- a/third_party/maul2d/src/ray_cast.c +++ b/third_party/maul2d/src/ray_cast.c @@ -179,7 +179,7 @@ static void KeepFirst(m2LocalHit* best, m2LocalHit candidate) } // True when p lies within radius of the polygon's core. -static bool InsideRounded(m2Vec2 p, const m2Polygon* poly) +M2_NODISCARD static bool InsideRounded(m2Vec2 p, const m2Polygon* poly) { float inside = -3.4e38f; float nearest = 3.4e38f; diff --git a/third_party/maul2d/src/rotation.h b/third_party/maul2d/src/rotation.h index aa01ba53e..98baa0845 100644 --- a/third_party/maul2d/src/rotation.h +++ b/third_party/maul2d/src/rotation.h @@ -3,13 +3,15 @@ // // The angle and rotation functions as inline code for the hot paths (the // pose update runs them for every body every substep). The public -// m2UnwindAngle, m2MakeRot, m2NormalizeRot and m2MulRot are these same -// functions, so the two can never differ by a bit. Only +, -, *, /, -// m2AbsF, m2MaxF and the IEEE-exact floorf and sqrtf are used. +// m2MakeRot, m2NormalizeRot and m2MulRot are these same functions, so the +// two can never differ by a bit. Only +, -, *, /, m2AbsF, m2MaxF, +// m2ClampF and the IEEE-exact floorf and sqrtf are used. #ifndef MAUL2D_SRC_ROTATION_H #define MAUL2D_SRC_ROTATION_H +#include "core.h" + #include "maul2d/base.h" #include "maul2d/core_math.h" diff --git a/third_party/maul2d/src/shape.c b/third_party/maul2d/src/shape.c index e4eb9e2cf..dbc0fa883 100644 --- a/third_party/maul2d/src/shape.c +++ b/third_party/maul2d/src/shape.c @@ -192,29 +192,18 @@ static void WriteShapeSlot(m2World* world, int32_t index, int32_t bodyIndex, con sh->shapeAlive[index] = 1; } -// Enters the shape into its body's tree. A full node pool undoes the -// create: the slot goes back to the head of the free ring with its -// generation untouched, since a refused create moves no later id. -static bool InsertShapeProxy(m2World* world, int32_t index, int32_t oldMaxShapeIndex) +// Enters the shape into its body's tree. The insert cannot fail: a tree +// holds 2L - 1 nodes for its L leaves, each live shape is at most one +// leaf, and the pool holds two nodes per shape slot (treeNodeCapacity). +static void InsertShapeProxy(m2World* world, int32_t index) { int32_t bodyIndex = world->shapes.shapeBody[index]; int32_t tree = world->bodies.types[bodyIndex]; world->broadphase.proxyIds[index] = m2TreeInsert(&world->broadphase.trees[tree], world->broadphase.treeNodes[tree], m2Fatten(m2ShapeTightAabb(world, index)), index); - if (world->broadphase.proxyIds[index] != M2_NULL_NODE) - { - m2PushMoved(world, index); - return true; - } - m2Shapes* sh = &world->shapes; - world->bodies.bodyShapeHead[bodyIndex] = sh->shapeNext[index]; - sh->shapeAlive[index] = 0; - sh->maxShapeIndex = oldMaxShapeIndex; - sh->shapeFreeHead = (sh->shapeFreeHead + sh->shapeCapacity - 1) % sh->shapeCapacity; - sh->shapeFreeQueue[sh->shapeFreeHead] = index; - sh->shapeFreeCount += 1; - return false; + M2_ASSERT(world->broadphase.proxyIds[index] != M2_NULL_NODE); + m2PushMoved(world, index); } // The create record holds the def's fields and zero bytes between them: @@ -256,14 +245,14 @@ m2Result m2CreateShape(m2World* world, m2BodyId bodyId, const m2ShapeDef* def, world->shapes.shapeFreeHead = (world->shapes.shapeFreeHead + 1) % world->shapes.shapeCapacity; world->shapes.shapeFreeCount -= 1; WriteShapeSlot(world, index, bodyIndex, def, geometry); - int32_t oldMaxShapeIndex = world->shapes.maxShapeIndex; - world->shapes.maxShapeIndex = index + 1 > oldMaxShapeIndex ? index + 1 : oldMaxShapeIndex; + world->shapes.maxShapeIndex = + index + 1 > world->shapes.maxShapeIndex ? index + 1 : world->shapes.maxShapeIndex; // Dormant bodies keep the shape out of the trees until Enable, but // everything else (mass, journaling, the id) proceeds normally so // replays mint identical worlds. - if (world->bodies.disabled[bodyIndex] == 0 && !InsertShapeProxy(world, index, oldMaxShapeIndex)) + if (world->bodies.disabled[bodyIndex] == 0) { - return m2Refuse(world, m2_errorCapacity); + InsertShapeProxy(world, index); } m2RecomputeMass(world, bodyIndex); m2ShapeId id = {(uint32_t)(index + 1), world->shapes.shapeGenerations[index]}; @@ -377,7 +366,7 @@ static int32_t ShapeSlotChecked(const m2World* world, m2ShapeId shapeId) // One journaled channel for shape materials (op 22). // The value contract of each shape parameter channel, shared by the // live setters and replay. -static bool ShapeParamValid(uint8_t param, float value) +M2_NODISCARD static bool ShapeParamValid(uint8_t param, float value) { switch (param) { @@ -458,7 +447,7 @@ static M2_NOINLINE void ShapeSetTangentSpeedWorker(m2World* world, m2ShapeId sha { return; } - m2SetShapeParamInternal(world, shapeId, m2_shapeParamTangentSpeed, speed); + (void)m2SetShapeParamInternal(world, shapeId, m2_shapeParamTangentSpeed, speed); } void m2Shape_SetTangentSpeed(m2World* world, m2ShapeId shapeId, float speed) @@ -495,7 +484,7 @@ static M2_NOINLINE void ShapeSetFrictionWorker(m2World* world, m2ShapeId shapeId { return; } - m2SetShapeParamInternal(world, shapeId, m2_shapeParamFriction, friction); + (void)m2SetShapeParamInternal(world, shapeId, m2_shapeParamFriction, friction); } void m2Shape_SetFriction(m2World* world, m2ShapeId shapeId, float friction) @@ -512,7 +501,7 @@ static M2_NOINLINE void ShapeSetRestitutionWorker(m2World* world, m2ShapeId shap { return; } - m2SetShapeParamInternal(world, shapeId, m2_shapeParamRestitution, restitution); + (void)m2SetShapeParamInternal(world, shapeId, m2_shapeParamRestitution, restitution); } void m2Shape_SetRestitution(m2World* world, m2ShapeId shapeId, float restitution) @@ -775,7 +764,8 @@ void m2Shape_SetSegment(m2World* world, m2ShapeId shapeId, const m2Segment* segm m2FloatEnvLeave(env); } -static M2_NOINLINE bool ShapeTestPointWorker(const m2World* world, m2ShapeId shapeId, m2Pos2 point) +M2_NODISCARD static M2_NOINLINE bool ShapeTestPointWorker(const m2World* world, m2ShapeId shapeId, + m2Pos2 point) { int32_t index = world != NULL ? m2ShapeSlot(world, shapeId) : -1; if (index < 0) diff --git a/third_party/maul2d/src/shape.h b/third_party/maul2d/src/shape.h index 13fc74bc8..297640cda 100644 --- a/third_party/maul2d/src/shape.h +++ b/third_party/maul2d/src/shape.h @@ -19,6 +19,7 @@ m2Result m2CreateShape(m2World* world, m2BodyId bodyId, const m2ShapeDef* def, #define M2_SHAPE_COOKIE (M2_COOKIE ^ ((int32_t)sizeof(m2ShapeDef) << 8) ^ 3) // The journaled parameter channel (codes in journal.h). -bool m2SetShapeParamInternal(m2World* world, m2ShapeId shapeId, uint8_t param, float value); +M2_NODISCARD bool m2SetShapeParamInternal(m2World* world, m2ShapeId shapeId, uint8_t param, + float value); #endif // MAUL2D_SRC_SHAPE_H diff --git a/third_party/maul2d/src/shape_read.c b/third_party/maul2d/src/shape_read.c index 1da7c3bb9..4e723d4c4 100644 --- a/third_party/maul2d/src/shape_read.c +++ b/third_party/maul2d/src/shape_read.c @@ -81,15 +81,16 @@ void m2Shape_GetFilter(const m2World* world, m2ShapeId shapeId, uint64_t* catego return world->shapes.shapeGeometry[index].field; \ } -M2_GEOMETRY_GETTER(m2Shape_GetCircle, m2Circle, circle, m2_circleShape) +M2_GEOMETRY_GETTER(m2CircleShape_GetCircle, m2Circle, circle, m2_circleShape) -M2_GEOMETRY_GETTER(m2Shape_GetCapsule, m2Capsule, capsule, m2_capsuleShape) +M2_GEOMETRY_GETTER(m2CapsuleShape_GetCapsule, m2Capsule, capsule, m2_capsuleShape) -M2_GEOMETRY_GETTER(m2Shape_GetPolygon, m2Polygon, polygon, m2_polygonShape) +M2_GEOMETRY_GETTER(m2PolygonShape_GetPolygon, m2Polygon, polygon, m2_polygonShape) -M2_GEOMETRY_GETTER(m2Shape_GetSegment, m2Segment, segment, m2_segmentShape) +M2_GEOMETRY_GETTER(m2SegmentShape_GetSegment, m2Segment, segment, m2_segmentShape) -M2_GEOMETRY_GETTER(m2Shape_GetChainSegment, m2ChainSegment, chainSegment, m2_chainSegmentShape) +M2_GEOMETRY_GETTER(m2ChainSegmentShape_GetChainSegment, m2ChainSegment, chainSegment, + m2_chainSegmentShape) #undef M2_GEOMETRY_GETTER diff --git a/third_party/maul2d/src/snapshot.c b/third_party/maul2d/src/snapshot.c index 180d747ea..156d22740 100644 --- a/third_party/maul2d/src/snapshot.c +++ b/third_party/maul2d/src/snapshot.c @@ -22,7 +22,7 @@ #define M2_SNAPSHOT_MAGIC M2_STATE_MAGIC('M', '2', 'S', 'N') -#define M2_SNAPSHOT_VERSION 7u +#define M2_SNAPSHOT_VERSION 9u // --- Snapshot ------------------------------------------------------------------- @@ -48,7 +48,7 @@ static void WorldCapacities(const m2World* world, int32_t capacities[6]) capacities[1] = world->shapes.shapeCapacity; capacities[2] = world->joints.jointCapacity; capacities[3] = world->particles.particleCapacity; - capacities[4] = world->volumes.fvCapacity; + capacities[4] = world->volumes.wvCapacity; capacities[5] = world->contacts.pairCapacity; } @@ -57,7 +57,7 @@ static uint64_t ConfigHash(void) // Everything that changes what the serialized bytes MEAN, and // nothing that does not (SIMD backend and worker count are // deliberately absent: the format is portable across them). The - // release that wrote the bytes is the prefix's, not part of this. + // ABI version that wrote the bytes is the prefix's, not part of this. uint64_t h = 0xCBF29CE484222325ull; int32_t version = (int32_t)M2_SNAPSHOT_VERSION; int32_t realSize = (int32_t)sizeof(float); @@ -91,15 +91,15 @@ bool m2SnapshotFits(const m2World* world) return SnapshotBytes(world) < INT64_MAX; } -static M2_NOINLINE int64_t WorldSnapshotSizeWorker(const m2World* world) +static M2_NOINLINE int64_t WorldGetSnapshotSizeWorker(const m2World* world) { return world != NULL ? SnapshotBytes(world) : 0; } -int64_t m2World_SnapshotSize(const m2World* world) +int64_t m2World_GetSnapshotSize(const m2World* world) { uint64_t env = m2FloatEnvEnter(); - int64_t result = WorldSnapshotSizeWorker(world); + int64_t result = WorldGetSnapshotSizeWorker(world); m2FloatEnvLeave(env); return result; } @@ -107,21 +107,34 @@ int64_t m2World_SnapshotSize(const m2World* world) // The block walk is shared by Snapshot and Restore so the layouts can // never drift apart (direction 0 = write, 1 = read). +// A snapshot that fails writes none of its bytes, so it zeroes the +// caller's buffer over its whole capacity: no bytes of an earlier snapshot +// are left to read as this one. +static void ClearSnapshotBuffer(void* buffer, int64_t capacity) +{ + if (buffer != NULL && capacity > 0 && m2SizeFitsMemory(capacity)) + { + memset(buffer, 0, (size_t)capacity); + } +} + static M2_NOINLINE m2Result WorldSnapshotWorker(const m2World* world, void* buffer, int64_t capacity, int64_t* bytesOut) { - if (bytesOut == NULL) + if (bytesOut == NULL || world == NULL || buffer == NULL || capacity < 0) { + ClearSnapshotBuffer(buffer, capacity); + if (bytesOut != NULL) + { + *bytesOut = 0; + } return m2Refuse(world, m2_errorInvalid); } *bytesOut = 0; - if (world == NULL || buffer == NULL) - { - return m2Refuse(world, m2_errorInvalid); - } int64_t size = SnapshotBytes(world); if (capacity < size) { + ClearSnapshotBuffer(buffer, capacity); *bytesOut = size; // the size a buffer must have return m2_errorCapacity; } @@ -143,10 +156,11 @@ static M2_NOINLINE m2Result WorldSnapshotWorker(const m2World* world, void* buff return m2_success; } -m2Result m2World_Snapshot(const m2World* world, void* buffer, int64_t capacity, int64_t* bytesOut) +m2Result m2World_Snapshot(const m2World* world, void* bufferOut, int64_t capacity, + int64_t* bytesOut) { uint64_t env = m2FloatEnvEnter(); - m2Result result = WorldSnapshotWorker(world, buffer, capacity, bytesOut); + m2Result result = WorldSnapshotWorker(world, bufferOut, capacity, bytesOut); m2FloatEnvLeave(env); return result; } @@ -164,7 +178,7 @@ static M2_NOINLINE m2Result WorldRestoreWorker(m2World* world, const void* buffe } // From here on the bytes are untrusted input: refusing them is no // misuse of the API, so nothing is counted. - // Only the release that wrote a snapshot reads it. + // Only the minor version that wrote a snapshot reads it. m2Result prefix = m2CheckStatePrefix(buffer, size, M2_SNAPSHOT_MAGIC, M2_SNAPSHOT_VERSION, (int32_t)sizeof(m2SnapshotHeader)); if (prefix != m2_success) @@ -203,14 +217,13 @@ static M2_NOINLINE m2Result WorldRestoreWorker(m2World* world, const void* buffe // The price is tape size, and that is the caller's tradeoff. m2JournalRecordRestore(world, buffer, size); - // Events are an observer stream from an abandoned timeline: cleared - // on restore, re-emitted by re-simulation. + // The last step's events belong to an abandoned timeline: cleared on + // restore, emitted again by the steps simulated again. The end events + // queued for the next step came with the snapshot. world->events.beginEventCount = 0; world->events.endEventCount = 0; - world->events.pendingEndCount = 0; world->events.sensorBeginCount = 0; world->events.sensorEndCount = 0; - world->events.pendingSensorEndCount = 0; world->events.jointBreakEventCount = 0; return m2_success; } diff --git a/third_party/maul2d/src/snapshot.h b/third_party/maul2d/src/snapshot.h index b8ecd2ad3..8a9ea207f 100644 --- a/third_party/maul2d/src/snapshot.h +++ b/third_party/maul2d/src/snapshot.h @@ -13,6 +13,6 @@ // Whether a world of these capacities has a snapshot size a size_t can // hold. It reads the capacities alone, so m2CreateWorld asks before it // allocates. Only a 32-bit target can say no. -bool m2SnapshotFits(const m2World* world); +M2_NODISCARD bool m2SnapshotFits(const m2World* world); #endif // MAUL2D_SRC_SNAPSHOT_H diff --git a/third_party/maul2d/src/snapshot_check.c b/third_party/maul2d/src/snapshot_check.c index 7132b7422..0f5acf7c6 100644 --- a/third_party/maul2d/src/snapshot_check.c +++ b/third_party/maul2d/src/snapshot_check.c @@ -14,78 +14,49 @@ // keeps there, in the ranges its def and setters give them; // - each body's mass data is what its shapes give it, bit for bit, or // an override m2Body_SetMassData would have accepted; +// - each live water volume's box has area, as its def must; // - each broadphase tree is a balanced tree whose leaves are exactly the // live shapes of enabled bodies of its type, with boxes that contain // their children, and its free list holds every other node; // - the moved list and its flags agree, and the pairs are ordered and // join live shapes in the broadphase; +// - each pair the next step keeps is one the broadphase would make, by +// the shapes' kinds, their bodies' types and filters and the joints +// between their bodies; +// - each pair's touching flag is its manifold's, and each frozen pair, +// both ends unmoved and static or asleep for two step ends, holds the +// manifold its shapes give at their bodies' poses: the step copies it +// forward instead of computing it; +// - each end event queued for the next step carries the step count and +// names two shape slots; +// - each static body and each sleeping body is still, and only a dynamic +// body sleeps; // - each shape of a static or sleeping body lies inside its fat box at // the body's pose: the step refits no such box before it, or a host, // queries it. -// The bytes may be unaligned, so every read goes through memcpy. Every +// The moved list, the pairs, the frozen manifolds and the queued end +// events are checked in snapshot_pairs.c. The bytes may be unaligned, so every read goes through +// memcpy (snapshot_read.h). Every // index read is bounded here too, though m2StateValidate already put each // one within its pool, so the check stands on its own. The only memory -// written is the world's restoreMarks scratch. +// written is the world's restoreMarks and pairScratch scratch. #include "snapshot_check.h" +#include "buoyancy.h" #include "core.h" #include "dynamic_tree.h" #include "geometry.h" #include "joint.h" #include "mass.h" +#include "snapshot_pairs.h" +#include "snapshot_read.h" #include "world_state.h" #include "maul2d/base.h" #include -// --- Reading the incoming bytes --------------------------------------------- - -static bool InPool(int32_t index, int32_t capacity) -{ - return index >= 0 && index < capacity; -} - -static int32_t ReadI32(const uint8_t* array, int32_t index) -{ - int32_t value; - memcpy(&value, array + (size_t)index * sizeof(value), sizeof(value)); - return value; -} - -static float ReadF32(const uint8_t* array, int32_t index) -{ - float value; - memcpy(&value, array + (size_t)index * sizeof(value), sizeof(value)); - return value; -} - -static uint32_t ReadU32(const uint8_t* array, int32_t index) -{ - uint32_t value; - memcpy(&value, array + (size_t)index * sizeof(value), sizeof(value)); - return value; -} - -static uint64_t ReadU64(const uint8_t* array, int32_t index) -{ - uint64_t value; - memcpy(&value, array + (size_t)index * sizeof(value), sizeof(value)); - return value; -} - -static m2TreeNode ReadNode(const uint8_t* nodes, int32_t index) -{ - m2TreeNode node; - memcpy(&node, nodes + (size_t)index * sizeof(node), sizeof(node)); - return node; -} - -// The incoming copy of a field; every field this file reads is snapshot -// state in every world that has its pool. -#define M2_IN(field) m2StateIncoming(world, in, (const void*)&world->field) - // --- Free queues ------------------------------------------------------------ // One pool's slots and its ring of free slots, as the incoming bytes have @@ -130,18 +101,21 @@ static int32_t CheckPool(uint8_t* marks, const Pool* pool) exhausted += 1; } } + // Each count is checked against the capacity before this, but their sum + // reaches three times a capacity no checked product bounds (bodies, + // joints, water volumes), so the sums are taken in 64 bits. int32_t cursors = pool->capacity > 0 ? pool->capacity : 1; if (!InPool(pool->head, cursors) || pool->count < 0 || exhausted != pool->retired || - live + pool->count + pool->retired != pool->capacity || - (pool->hasTail && - (!InPool(pool->tail, cursors) || pool->tail != (pool->head + pool->count) % cursors))) + (int64_t)live + pool->count + pool->retired != pool->capacity || + (pool->hasTail && (!InPool(pool->tail, cursors) || + pool->tail != ((int64_t)pool->head + pool->count) % cursors))) { return -1; } memset(marks, 0, (size_t)pool->capacity); for (int32_t k = 0; k < pool->count; ++k) { - int32_t slot = ReadI32(pool->queue, (pool->head + k) % pool->capacity); + int32_t slot = ReadI32(pool->queue, (int32_t)(((int64_t)pool->head + k) % pool->capacity)); if (!InPool(slot, pool->capacity)) { return -1; @@ -156,15 +130,11 @@ static int32_t CheckPool(uint8_t* marks, const Pool* pool) return live; } -static int32_t InlineI32(const m2World* world, const uint8_t* in, const void* field) -{ - return ReadI32(m2StateIncoming(world, in, field), 0); -} - // Each end of `count` links, `ends` arrays of particle slots, is a live // particle. -static bool LinksJoinLive(const m2World* world, const uint8_t* alive, const uint8_t* const* ends, - int32_t endCount, int32_t count, int32_t capacity) +M2_NODISCARD static bool LinksJoinLive(const m2World* world, const uint8_t* alive, + const uint8_t* const* ends, int32_t endCount, int32_t count, + int32_t capacity) { if (count < 0 || count > capacity) { @@ -189,7 +159,7 @@ static bool LinksJoinLive(const m2World* world, const uint8_t* alive, const uint // corners within one lattice stride of their centroid on each axis. The // header has matched the snapshot's particle radius to this world's, and // the counts are already in range. -static bool LinkShapesInRange(const m2World* world, const uint8_t* in) +M2_NODISCARD static bool LinkShapesInRange(const m2World* world, const uint8_t* in) { float diameter = 2.0f * world->particles.particleRadius; float stride = 0.75f * diameter; // the fill's lattice stride @@ -222,10 +192,46 @@ static bool LinkShapesInRange(const m2World* world, const uint8_t* in) return true; } +// Every flag byte or word holds only the bits the library defines: a +// joint's flags, a body's linear locks and, in a world with particles, +// each particle's behavior flags. +M2_NODISCARD static bool CheckFlagBits(const m2World* world, const uint8_t* in) +{ + const uint8_t* jointFlags = M2_IN(joints.jointFlags); + for (int32_t j = 0; j < world->joints.jointCapacity; ++j) + { + if ((jointFlags[j] & ~M2_JOINT_FLAGS) != 0) + { + return false; + } + } + const uint8_t* locks = M2_IN(bodies.motionLocks); + for (int32_t b = 0; b < world->bodies.bodyCapacity; ++b) + { + if ((locks[b] & ~M2_LOCKS) != 0) + { + return false; + } + } + if (world->particles.particleCapacity == 0) + { + return true; + } + const uint8_t* particleFlags = M2_IN(particles.particleFlags); + for (int32_t p = 0; p < world->particles.particleCapacity; ++p) + { + if ((ReadU32(particleFlags, p) & ~M2_PARTICLE_FLAGS) != 0) + { + return false; + } + } + return true; +} + // Springs and triads join live particles only, as a particle's death // leaves them: one with a dead end would push that slot's velocity and // latch onto the next particle emitted there. -static bool CheckParticleLinks(const m2World* world, const uint8_t* in) +M2_NODISCARD static bool CheckParticleLinks(const m2World* world, const uint8_t* in) { const uint8_t* alive = M2_IN(particles.particleAlive); const uint8_t* springs[2] = {M2_IN(particles.particleSpringA), @@ -241,9 +247,29 @@ static bool CheckParticleLinks(const m2World* world, const uint8_t* in) LinkShapesInRange(world, in); } -// The particle and fluid volume pools, which exist only in worlds that +// Each live water volume's box passes the test m2CreateWaterVolume puts its +// def's box to (m2WaterBoxValid), since no call changes the box later. +M2_NODISCARD static bool VolumeBoxesHaveArea(const m2World* world, const uint8_t* in) +{ + const uint8_t* alive = M2_IN(volumes.wvAlive); + const uint8_t* lowers = M2_IN(volumes.wvLower); + const uint8_t* uppers = M2_IN(volumes.wvUpper); + for (int32_t v = 0; v < world->volumes.wvCapacity; ++v) + { + m2WorldAabb box; + memcpy(&box.lowerBound, lowers + (size_t)v * sizeof(m2Pos2), sizeof(m2Pos2)); + memcpy(&box.upperBound, uppers + (size_t)v * sizeof(m2Pos2), sizeof(m2Pos2)); + if (alive[v] != 0 && !m2WaterBoxValid(box)) + { + return false; + } + } + return true; +} + +// The particle and water volume pools, which exist only in worlds that // have them. -static bool CheckFluidPools(m2World* world, const uint8_t* in) +M2_NODISCARD static bool CheckFluidPools(m2World* world, const uint8_t* in) { uint8_t* marks = world->restoreMarks; if (world->particles.particleCapacity > 0) @@ -265,19 +291,19 @@ static bool CheckFluidPools(m2World* world, const uint8_t* in) return false; } } - if (world->volumes.fvCapacity > 0) + if (world->volumes.wvCapacity > 0) { - const Pool volumes = {M2_IN(volumes.fvAlive), - M2_IN(volumes.fvGenerations), - M2_IN(volumes.fvFreeQueue), - world->volumes.fvCapacity, - InlineI32(world, in, &world->volumes.fvFreeHead), - InlineI32(world, in, &world->volumes.fvFreeCount), + const Pool volumes = {M2_IN(volumes.wvAlive), + M2_IN(volumes.wvGenerations), + M2_IN(volumes.wvFreeQueue), + world->volumes.wvCapacity, + InlineI32(world, in, &world->volumes.wvFreeHead), + InlineI32(world, in, &world->volumes.wvFreeCount), false, 0, - InlineI32(world, in, &world->volumes.fvRetiredCount), - InlineI32(world, in, &world->volumes.maxFvIndex)}; - if (CheckPool(marks, &volumes) < 0) + InlineI32(world, in, &world->volumes.wvRetiredCount), + InlineI32(world, in, &world->volumes.maxWvIndex)}; + if (CheckPool(marks, &volumes) < 0 || !VolumeBoxesHaveArea(world, in)) { return false; } @@ -285,7 +311,7 @@ static bool CheckFluidPools(m2World* world, const uint8_t* in) return true; } -static bool CheckPools(m2World* world, const uint8_t* in) +M2_NODISCARD static bool CheckPools(m2World* world, const uint8_t* in) { uint8_t* marks = world->restoreMarks; const Pool bodies = {M2_IN(bodies.alive), @@ -341,7 +367,7 @@ static bool CheckPools(m2World* world, const uint8_t* in) // Each live body's shape list holds only its own live shapes and ends // within the pool; together the lists hold every live shape, so no shape // is on two lists and none is missing. A dead body owns no list. -static bool CheckShapeLists(const m2World* world, const uint8_t* in) +M2_NODISCARD static bool CheckShapeLists(const m2World* world, const uint8_t* in) { const uint8_t* bodyAlive = M2_IN(bodies.alive); const uint8_t* heads = M2_IN(bodies.bodyShapeHead); @@ -378,7 +404,7 @@ static bool CheckShapeLists(const m2World* world, const uint8_t* in) // A live chain belongs to a live body, and a live shape of a chain shares // the chain's body. -static bool CheckChains(const m2World* world, const uint8_t* in) +M2_NODISCARD static bool CheckChains(const m2World* world, const uint8_t* in) { const uint8_t* bodyAlive = M2_IN(bodies.alive); const uint8_t* chainAlive = M2_IN(chains.chainAlive); @@ -424,7 +450,7 @@ typedef struct JointFields // The motor and limit kinds keep angle limits (revolute) or translation // limits (prismatic, wheel) in lower and upper. -static bool LimitsValid(const JointFields* f) +M2_NODISCARD static bool LimitsValid(const JointFields* f) { if (f->type == (uint8_t)m2_revoluteJoint) { @@ -440,7 +466,7 @@ static bool LimitsValid(const JointFields* f) // Accumulated state the step grows without a bound (a gear's phase in // reference, a ratchet's angle and tooth in upper and lower) stays any // finite value, so every snapshot a world writes restores. -static bool JointKindValid(const JointFields* f) +M2_NODISCARD static bool JointKindValid(const JointFields* f) { switch ((m2JointType)f->type) { @@ -478,7 +504,7 @@ static bool JointKindValid(const JointFields* f) return false; } -static bool CheckJoints(const m2World* world, const uint8_t* in) +M2_NODISCARD static bool CheckJoints(const m2World* world, const uint8_t* in) { const uint8_t* bodyAlive = M2_IN(bodies.alive); const uint8_t* jointAlive = M2_IN(joints.jointAlive); @@ -516,7 +542,7 @@ static bool CheckJoints(const m2World* world, const uint8_t* in) // Every live shape's geometry is one the library would keep: valid for its // type, and a polygon exactly as the library builds it from its vertices, // normals included. -static bool CheckGeometry(const m2World* world, const uint8_t* in) +M2_NODISCARD static bool CheckGeometry(const m2World* world, const uint8_t* in) { const uint8_t* shapeAlive = M2_IN(shapes.shapeAlive); const uint8_t* geometries = M2_IN(shapes.shapeGeometry); @@ -563,7 +589,8 @@ static m2MassData IncomingShapeMass(const void* context, int32_t shape) // Mass data set by m2Body_SetMassData: a dynamic body's, with inverses a // float can invert back, as the setter's own rules leave them. -static bool OverrideValid(float invMass, float invInertia, bool dynamic, bool fixedRotation) +M2_NODISCARD static bool OverrideValid(float invMass, float invInertia, bool dynamic, + bool fixedRotation) { return dynamic && m2MassInversesValid(invMass, invInertia) && (!fixedRotation || invInertia == 0.0f); @@ -571,7 +598,7 @@ static bool OverrideValid(float invMass, float invInertia, bool dynamic, bool fi // Every live body's mass data is what its shapes give it, bit for bit, or // a valid override: derived data a world would never hold cannot land. -static bool CheckMass(const m2World* world, const uint8_t* in) +M2_NODISCARD static bool CheckMass(const m2World* world, const uint8_t* in) { const uint8_t* alive = M2_IN(bodies.alive); const uint8_t* types = M2_IN(bodies.types); @@ -627,8 +654,8 @@ typedef struct Leaves // A node as reached from its parent: its own rules, and its children's // links back to it. -static bool CheckNode(const uint8_t* nodes, int32_t capacity, int32_t index, int32_t tree, - const Leaves* leaves) +M2_NODISCARD static bool CheckNode(const uint8_t* nodes, int32_t capacity, int32_t index, + int32_t tree, const Leaves* leaves) { m2TreeNode node = ReadNode(nodes, index); if (node.height == 0) @@ -731,7 +758,7 @@ static int32_t WalkFreeList(const uint8_t* nodes, int32_t capacity, int32_t head // exactly the ones the walk visits, and the free list holds the rest. Each // live shape of an enabled body has a leaf in its body type's tree, and no // other shape has a proxy. -static bool CheckTrees(const m2World* world, const uint8_t* in) +M2_NODISCARD static bool CheckTrees(const m2World* world, const uint8_t* in) { const Leaves leaves = {M2_IN(shapes.shapeAlive), M2_IN(shapes.shapeBody), M2_IN(broadphase.proxyIds), M2_IN(bodies.types), @@ -781,65 +808,49 @@ static bool CheckTrees(const m2World* world, const uint8_t* in) return true; } -// --- Moved shapes and pairs ------------------------------------------------- - -// The moved list holds distinct shapes, and the flags mark exactly them. -static bool CheckMoved(m2World* world, const uint8_t* in) +// Bodies the world holds still: a static body never moves, since its def +// carries no velocity, the velocity setters refuse it and a type change to +// static zeroes its velocities; a sleeping body is dynamic and still, since +// only a dynamic body sleeps, both ways into sleep zero its velocities and +// every call that moves a body wakes it. The step reads both: the solver +// takes a static body's velocity, and waking keeps a sleeper's. +M2_NODISCARD static bool CheckStillBodies(const m2World* world, const uint8_t* in) { - const uint8_t* moved = M2_IN(broadphase.moved); - const uint8_t* inMoved = M2_IN(broadphase.inMoved); - int32_t count = InlineI32(world, in, &world->broadphase.movedCount); - if (count < 0 || count > world->shapes.shapeCapacity) - { - return false; - } - uint8_t* marks = world->restoreMarks; - memset(marks, 0, (size_t)world->shapes.shapeCapacity); - for (int32_t k = 0; k < count; ++k) + const uint8_t* alive = M2_IN(bodies.alive); + const uint8_t* types = M2_IN(bodies.types); + const uint8_t* asleep = M2_IN(bodies.asleep); + const uint8_t* linear = M2_IN(bodies.linearVelocities); + const uint8_t* angular = M2_IN(bodies.angularVelocities); + for (int32_t b = 0; b < world->bodies.bodyCapacity; ++b) { - int32_t s = ReadI32(moved, k); - if (!InPool(s, world->shapes.shapeCapacity) || marks[s] != 0 || inMoved[s] == 0) + bool still = types[b] == (uint8_t)m2_staticBody || asleep[b] != 0; + if (alive[b] == 0 || !still) + { + continue; + } + // An m2Vec2 is two floats. + bool moves = ReadF32(linear, 2 * b) != 0.0f || ReadF32(linear, 2 * b + 1) != 0.0f || + ReadF32(angular, b) != 0.0f; + if (moves || (asleep[b] != 0 && types[b] != (uint8_t)m2_dynamicBody)) { return false; } - marks[s] = 1; - } - int32_t flagged = 0; - for (int32_t s = 0; s < world->shapes.shapeCapacity; ++s) - { - flagged += inMoved[s] != 0 ? 1 : 0; } - return flagged == count; + return true; } -// Pairs are in strictly increasing key order, and each joins two live -// shapes of different bodies, both in the broadphase. -static bool CheckPairs(const m2World* world, const uint8_t* in) +// Every live body keeps a positive sleep threshold: its def and its +// setter store the default for 0. With 0, a body would never rest. +M2_NODISCARD static bool CheckSleepThresholds(const m2World* world, const uint8_t* in) { - const uint8_t* keys = M2_IN(contacts.pairKeys); - const uint8_t* touching = M2_IN(contacts.pairTouching); - const uint8_t* shapeAlive = M2_IN(shapes.shapeAlive); - const uint8_t* shapeBody = M2_IN(shapes.shapeBody); - const uint8_t* proxyIds = M2_IN(broadphase.proxyIds); - int32_t count = InlineI32(world, in, &world->contacts.pairCount); - if (count < 0 || count > world->contacts.pairCapacity) - { - return false; - } - uint64_t previous = 0; - for (int32_t i = 0; i < count; ++i) + const uint8_t* alive = M2_IN(bodies.alive); + const uint8_t* thresholds = M2_IN(bodies.sleepThresholds); + for (int32_t b = 0; b < world->bodies.bodyCapacity; ++b) { - uint64_t key = ReadU64(keys, i); - int32_t a = (int32_t)(key >> 32); - int32_t b = (int32_t)(key & 0xFFFFFFFFu); - if (a < 0 || b >= world->shapes.shapeCapacity || (i > 0 && key <= previous) || a >= b || - touching[i] > 1 || shapeAlive[a] == 0 || shapeAlive[b] == 0 || - ReadI32(proxyIds, a) == M2_NULL_NODE || ReadI32(proxyIds, b) == M2_NULL_NODE || - ReadI32(shapeBody, a) == ReadI32(shapeBody, b)) + if (alive[b] != 0 && !(ReadF32(thresholds, b) > 0.0f)) { return false; } - previous = key; } return true; } @@ -850,7 +861,7 @@ static bool CheckPairs(const m2World* world, const uint8_t* in) // shape's tight box at the body's pose, by the step's own computation and // test. Awake bodies' boxes lag their poses by the last step in every // snapshot, and the next step refits them first. -static bool CheckRestingBoxes(const m2World* world, const uint8_t* in) +M2_NODISCARD static bool CheckRestingBoxes(const m2World* world, const uint8_t* in) { const uint8_t* alive = M2_IN(shapes.shapeAlive); const uint8_t* owners = M2_IN(shapes.shapeBody); @@ -894,6 +905,7 @@ bool m2SnapshotCheck(m2World* world, const uint8_t* in) { return CheckPools(world, in) && CheckShapeLists(world, in) && CheckChains(world, in) && CheckJoints(world, in) && CheckGeometry(world, in) && CheckMass(world, in) && - CheckTrees(world, in) && CheckMoved(world, in) && CheckPairs(world, in) && - CheckRestingBoxes(world, in); + CheckTrees(world, in) && m2SnapshotCheckPairs(world, in) && + CheckRestingBoxes(world, in) && CheckStillBodies(world, in) && + CheckSleepThresholds(world, in) && CheckFlagBits(world, in); } diff --git a/third_party/maul2d/src/snapshot_check.h b/third_party/maul2d/src/snapshot_check.h index a0737cd93..d4ffb5ce9 100644 --- a/third_party/maul2d/src/snapshot_check.h +++ b/third_party/maul2d/src/snapshot_check.h @@ -16,7 +16,8 @@ // True when the block, already accepted by m2StateValidate, describes a // world no later call or step can drive out of its pools: free queues, // owner lists, the broadphase trees, the moved list and the pairs all -// agree. Reads the block in place and writes only world->restoreMarks. -bool m2SnapshotCheck(m2World* world, const uint8_t* in); +// agree. Reads the block in place and writes only world->restoreMarks and +// world->contacts.pairScratch. +M2_NODISCARD bool m2SnapshotCheck(m2World* world, const uint8_t* in); #endif // MAUL2D_SRC_SNAPSHOT_CHECK_H diff --git a/third_party/maul2d/src/snapshot_pairs.c b/third_party/maul2d/src/snapshot_pairs.c new file mode 100644 index 000000000..390bdf0f9 --- /dev/null +++ b/third_party/maul2d/src/snapshot_pairs.c @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Sirac Ozmen +// +// The pair part of an incoming snapshot's structural check +// (snapshot_check.c): the moved list, the pairs, the manifolds of frozen +// pairs and the end events queued for the next step. Like the rest of the check, it reads the bytes +// in place and bounds every index it reads. + +#include "snapshot_pairs.h" + +#include "broadphase.h" +#include "contact.h" +#include "geometry.h" +#include "snapshot_read.h" +#include "world_state.h" + +#include "maul2d/base.h" + +#include +#include +#include + +// The moved list holds distinct shapes, and the flags mark exactly them. +M2_NODISCARD static bool CheckMoved(m2World* world, const uint8_t* in) +{ + const uint8_t* moved = M2_IN(broadphase.moved); + const uint8_t* inMoved = M2_IN(broadphase.inMoved); + int32_t count = InlineI32(world, in, &world->broadphase.movedCount); + if (count < 0 || count > world->shapes.shapeCapacity) + { + return false; + } + uint8_t* marks = world->restoreMarks; + memset(marks, 0, (size_t)world->shapes.shapeCapacity); + for (int32_t k = 0; k < count; ++k) + { + int32_t s = ReadI32(moved, k); + if (!InPool(s, world->shapes.shapeCapacity) || marks[s] != 0 || inMoved[s] == 0) + { + return false; + } + marks[s] = 1; + } + int32_t flagged = 0; + for (int32_t s = 0; s < world->shapes.shapeCapacity; ++s) + { + flagged += inMoved[s] != 0 ? 1 : 0; + } + return flagged == count; +} + +// Pairs are in strictly increasing key order, and each joins two live +// shapes of different bodies, both in the broadphase. +M2_NODISCARD static bool CheckPairs(const m2World* world, const uint8_t* in) +{ + const uint8_t* keys = M2_IN(contacts.pairKeys); + const uint8_t* touching = M2_IN(contacts.pairTouching); + const uint8_t* shapeAlive = M2_IN(shapes.shapeAlive); + const uint8_t* shapeBody = M2_IN(shapes.shapeBody); + const uint8_t* proxyIds = M2_IN(broadphase.proxyIds); + int32_t count = InlineI32(world, in, &world->contacts.pairCount); + if (count < 0 || count > world->contacts.pairCapacity) + { + return false; + } + uint64_t previous = 0; + for (int32_t i = 0; i < count; ++i) + { + uint64_t key = ReadU64(keys, i); + int32_t a = (int32_t)(key >> 32); + int32_t b = (int32_t)(key & 0xFFFFFFFFu); + if (a < 0 || b >= world->shapes.shapeCapacity || (i > 0 && key <= previous) || a >= b || + touching[i] > 1 || shapeAlive[a] == 0 || shapeAlive[b] == 0 || + ReadI32(proxyIds, a) == M2_NULL_NODE || ReadI32(proxyIds, b) == M2_NULL_NODE || + ReadI32(shapeBody, a) == ReadI32(shapeBody, b)) + { + return false; + } + previous = key; + } + return true; +} + +// One shape as the pair rules read it, from the incoming bytes. +typedef struct IncomingFilters +{ + const uint8_t* categories; // uint64 per shape + const uint8_t* masks; // uint64 per shape + const uint8_t* groups; // int32 per shape + const uint8_t* sensors; + const uint8_t* geometries; + const uint8_t* shapeBody; + const uint8_t* bodyTypes; +} IncomingFilters; + +// The shape is live, so its body is live and in range: CheckPairs and +// CheckShapeLists ran first. +static m2PairSide IncomingSide(const IncomingFilters* f, int32_t shape) +{ + int32_t type; + memcpy(&type, + f->geometries + (size_t)shape * sizeof(m2ShapeGeometry) + + offsetof(m2ShapeGeometry, type), + sizeof(type)); + int32_t body = ReadI32(f->shapeBody, shape); + m2PairSide side = {ReadU64(f->categories, shape), ReadU64(f->masks, shape), + ReadI32(f->groups, shape), type, + f->sensors[shape] != 0, f->bodyTypes[body]}; + return side; +} + +static uint64_t BodyPairKey(int32_t a, int32_t b) +{ + return a < b ? ((uint64_t)(uint32_t)a << 32) | (uint32_t)b + : ((uint64_t)(uint32_t)b << 32) | (uint32_t)a; +} + +static int CompareKeys(const void* a, const void* b) +{ + uint64_t x; + uint64_t y; + memcpy(&x, a, sizeof(x)); + memcpy(&y, b, sizeof(y)); + return (x > y) - (x < y); +} + +// Whether `key`, two bodies, is among the `count` sorted keys. +M2_NODISCARD static bool HasKey(const uint64_t* keys, int32_t count, uint64_t key) +{ + int32_t lo = 0; + int32_t hi = count - 1; + while (lo <= hi) + { + int32_t mid = lo + (hi - lo) / 2; + if (keys[mid] == key) + { + return true; + } + if (keys[mid] < key) + { + lo = mid + 1; + } + else + { + hi = mid - 1; + } + } + return false; +} + +// The pairs the next step keeps, those with no moved shape, are pairs the +// broadphase would make: their kinds, their bodies' types and their +// filters let them pair, and no joint without connected collision joins +// their bodies. A host call that changes any of these between steps marks +// the shapes moved, and the step drops their pairs before it reads one. +// The kept pairs' bodies go into pairScratch, sorted, for the joints to +// look up. +M2_NODISCARD static bool CheckKeptPairs(m2World* world, const uint8_t* in) +{ + const IncomingFilters filters = {M2_IN(shapes.shapeCategory), M2_IN(shapes.shapeMask), + M2_IN(shapes.shapeGroup), M2_IN(shapes.shapeSensor), + M2_IN(shapes.shapeGeometry), M2_IN(shapes.shapeBody), + M2_IN(bodies.types)}; + const uint8_t* keys = M2_IN(contacts.pairKeys); + const uint8_t* inMoved = M2_IN(broadphase.inMoved); + int32_t count = InlineI32(world, in, &world->contacts.pairCount); + uint64_t* bodyPairs = world->contacts.pairScratch; + int32_t kept = 0; + for (int32_t i = 0; i < count; ++i) + { + uint64_t key = ReadU64(keys, i); + int32_t a = (int32_t)(key >> 32); + int32_t b = (int32_t)(key & 0xFFFFFFFFu); + if (inMoved[a] != 0 || inMoved[b] != 0) + { + continue; + } + m2PairSide sideA = IncomingSide(&filters, a); + m2PairSide sideB = IncomingSide(&filters, b); + if (!m2SidesMayPair(&sideA, &sideB)) + { + return false; + } + bodyPairs[kept] = BodyPairKey(ReadI32(filters.shapeBody, a), ReadI32(filters.shapeBody, b)); + kept += 1; + } + qsort(bodyPairs, (size_t)kept, sizeof(uint64_t), CompareKeys); + const uint8_t* jointAlive = M2_IN(joints.jointAlive); + const uint8_t* collide = M2_IN(joints.jointCollide); + const uint8_t* bodyA = M2_IN(joints.jointBodyA); + const uint8_t* bodyB = M2_IN(joints.jointBodyB); + for (int32_t j = 0; j < world->joints.jointCapacity; ++j) + { + if (jointAlive[j] != 0 && collide[j] == 0 && + HasKey(bodyPairs, kept, BodyPairKey(ReadI32(bodyA, j), ReadI32(bodyB, j)))) + { + return false; + } + } + return true; +} + +// The incoming arrays that say whether a shape's end of a pair is frozen, +// and what its manifold is computed from. +typedef struct IncomingEnds +{ + const uint8_t* inMoved; + const uint8_t* shapeBody; + const uint8_t* geometries; + const uint8_t* types; + const uint8_t* asleep; + const uint8_t* sleepStreak; + const uint8_t* transforms; +} IncomingEnds; + +// A frozen end, as the step's contact update reads one: not moved since +// the last step, and static or asleep for two step ends. The shape is +// live, so its body is live and in range: CheckPairs and CheckShapeLists +// ran first. +M2_NODISCARD static bool IncomingFrozen(const IncomingEnds* e, int32_t shape) +{ + int32_t body = ReadI32(e->shapeBody, shape); + return e->inMoved[shape] == 0 && (e->types[body] == (uint8_t)m2_staticBody || + (e->types[body] == (uint8_t)m2_dynamicBody && + e->asleep[body] != 0 && e->sleepStreak[body] >= 2)); +} + +// The manifold the step computes for a pair of shapes at their bodies' +// incoming poses. +static m2Manifold IncomingManifold(const IncomingEnds* e, int32_t a, int32_t b) +{ + m2ShapeGeometry ga; + m2ShapeGeometry gb; + memcpy(&ga, e->geometries + (size_t)a * sizeof(ga), sizeof(ga)); + memcpy(&gb, e->geometries + (size_t)b * sizeof(gb), sizeof(gb)); + m2Transform xfA; + m2Transform xfB; + memcpy(&xfA, e->transforms + (size_t)ReadI32(e->shapeBody, a) * sizeof(xfA), sizeof(xfA)); + memcpy(&xfB, e->transforms + (size_t)ReadI32(e->shapeBody, b) * sizeof(xfB), sizeof(xfB)); + return m2ComputeManifold(&ga, &gb, m2MakeRelativePose(xfA, xfB)); +} + +// Whether two floats have the same bits: -0 differs from 0, and a NaN +// matches only its own pattern. +M2_NODISCARD static bool SameBits(float x, float y) +{ + uint32_t a; + uint32_t b; + memcpy(&a, &x, sizeof(a)); + memcpy(&b, &y, sizeof(b)); + return a == b; +} + +M2_NODISCARD static bool SameVecBits(m2Vec2 x, m2Vec2 y) +{ + return SameBits(x.x, y.x) && SameBits(x.y, y.y); +} + +// Whether two manifolds hold the same contact, bit for bit: the normal and +// each point's anchors, separation and feature id. The impulses and the +// persisted bit are left out. +M2_NODISCARD static bool SameContact(const m2Manifold* x, const m2Manifold* y) +{ + if (x->pointCount != y->pointCount || !SameVecBits(x->normal, y->normal)) + { + return false; + } + for (int32_t k = 0; k < x->pointCount; ++k) + { + const m2ManifoldPoint* p = &x->points[k]; + const m2ManifoldPoint* q = &y->points[k]; + if (!SameVecBits(p->anchorA, q->anchorA) || !SameVecBits(p->anchorB, q->anchorB) || + !SameBits(p->separation, q->separation) || p->id != q->id || + (p->flags & ~1u) != (q->flags & ~1u)) + { + return false; + } + } + return true; +} + +// Each pair's touching flag is its manifold's: the step sets every flag +// from the point count, and nothing between steps changes one without +// the other. A frozen pair holds the manifold its shapes give at their +// bodies' poses, since the step copies it forward instead of computing +// it; only its impulses, which the solver accumulates, and the persisted +// bit are the snapshot's own. Every snapshot a world writes keeps the +// rule: a host call that moves a shape wakes its sleeping body or marks +// the shape moved, and a body falling asleep computes its manifolds one +// more step before they freeze. +M2_NODISCARD static bool CheckFrozenManifolds(const m2World* world, const uint8_t* in) +{ + const IncomingEnds ends = {M2_IN(broadphase.inMoved), M2_IN(shapes.shapeBody), + M2_IN(shapes.shapeGeometry), M2_IN(bodies.types), + M2_IN(bodies.asleep), M2_IN(bodies.sleepStreak), + M2_IN(bodies.transforms)}; + const uint8_t* keys = M2_IN(contacts.pairKeys); + const uint8_t* touching = M2_IN(contacts.pairTouching); + const uint8_t* manifolds = M2_IN(contacts.manifolds); + int32_t count = InlineI32(world, in, &world->contacts.pairCount); + for (int32_t i = 0; i < count; ++i) + { + m2Manifold stored; + memcpy(&stored, manifolds + (size_t)i * sizeof(stored), sizeof(stored)); + if ((touching[i] != 0) != (stored.pointCount > 0)) + { + return false; + } + uint64_t key = ReadU64(keys, i); + int32_t a = (int32_t)(key >> 32); + int32_t b = (int32_t)(key & 0xFFFFFFFFu); + if (!IncomingFrozen(&ends, a) || !IncomingFrozen(&ends, b)) + { + continue; + } + m2Manifold fresh = IncomingManifold(&ends, a, b); + if (!SameContact(&stored, &fresh)) + { + return false; + } + } + return true; +} + +// Whether `count` queued end events, laid out as m2ContactEndEvent and +// m2SensorEndEvent both are, each carry the step count and name two shape +// slots of this world. +M2_NODISCARD static bool QueuedEndsValid(const m2World* world, const uint8_t* events, int32_t count, + uint64_t step) +{ + if (count < 0 || count > world->contacts.pairCapacity) + { + return false; + } + uint32_t capacity = (uint32_t)world->shapes.shapeCapacity; + for (int32_t i = 0; i < count; ++i) + { + m2ContactEndEvent e; + memcpy(&e, events + (size_t)i * sizeof(e), sizeof(e)); + if (e.shapeIdA.index1 < 1 || e.shapeIdA.index1 > capacity || e.shapeIdB.index1 < 1 || + e.shapeIdB.index1 > capacity || e.step != step) + { + return false; + } + } + return true; +} + +// The end events a destroy or a disable queued since the last step: each +// was queued at the snapshot's step count and names two shape slots, so +// the next step hands the host no id outside its pools. +M2_NODISCARD static bool CheckQueuedEnds(const m2World* world, const uint8_t* in) +{ + _Static_assert(sizeof(m2ContactEndEvent) == sizeof(m2SensorEndEvent), + "both queues share one layout"); + uint64_t step = ReadU64(M2_IN(stepCount), 0); + return QueuedEndsValid(world, M2_IN(events.pendingEndEvents), + InlineI32(world, in, &world->events.pendingEndCount), step) && + QueuedEndsValid(world, M2_IN(events.pendingSensorEnd), + InlineI32(world, in, &world->events.pendingSensorEndCount), step); +} + +#undef M2_IN + +bool m2SnapshotCheckPairs(m2World* world, const uint8_t* in) +{ + return CheckMoved(world, in) && CheckPairs(world, in) && CheckKeptPairs(world, in) && + CheckFrozenManifolds(world, in) && CheckQueuedEnds(world, in); +} diff --git a/third_party/maul2d/src/snapshot_pairs.h b/third_party/maul2d/src/snapshot_pairs.h new file mode 100644 index 000000000..444fb1576 --- /dev/null +++ b/third_party/maul2d/src/snapshot_pairs.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Sirac Ozmen +// +// The pair part of an incoming snapshot's structural check. + +#ifndef MAUL2D_SRC_SNAPSHOT_PAIRS_H +#define MAUL2D_SRC_SNAPSHOT_PAIRS_H + +#include "world_internal.h" + +#include +#include + +// True when the block's moved list matches its flags, its pairs are in +// order, join live shapes in the broadphase and, where the next step keeps +// them, are pairs the broadphase would make, its touching flags and frozen +// pairs' manifolds are those its shapes give, and its queued end events +// carry the step count and name shape slots. The trees and shape lists +// have passed m2SnapshotCheck first. Writes only world->restoreMarks and +// world->contacts.pairScratch. +M2_NODISCARD bool m2SnapshotCheckPairs(m2World* world, const uint8_t* in); + +#endif // MAUL2D_SRC_SNAPSHOT_PAIRS_H diff --git a/third_party/maul2d/src/snapshot_read.h b/third_party/maul2d/src/snapshot_read.h new file mode 100644 index 000000000..65b104467 --- /dev/null +++ b/third_party/maul2d/src/snapshot_read.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Sirac Ozmen +// +// Reads of an incoming snapshot block for its structural check. The bytes +// may be unaligned, so every read goes through memcpy. + +#ifndef MAUL2D_SRC_SNAPSHOT_READ_H +#define MAUL2D_SRC_SNAPSHOT_READ_H + +#include "dynamic_tree.h" +#include "world_internal.h" +#include "world_state.h" + +#include +#include +#include +#include + +M2_NODISCARD static inline bool InPool(int32_t index, int32_t capacity) +{ + return index >= 0 && index < capacity; +} + +static inline int32_t ReadI32(const uint8_t* array, int32_t index) +{ + int32_t value; + memcpy(&value, array + (size_t)index * sizeof(value), sizeof(value)); + return value; +} + +static inline float ReadF32(const uint8_t* array, int32_t index) +{ + float value; + memcpy(&value, array + (size_t)index * sizeof(value), sizeof(value)); + return value; +} + +static inline uint32_t ReadU32(const uint8_t* array, int32_t index) +{ + uint32_t value; + memcpy(&value, array + (size_t)index * sizeof(value), sizeof(value)); + return value; +} + +static inline uint64_t ReadU64(const uint8_t* array, int32_t index) +{ + uint64_t value; + memcpy(&value, array + (size_t)index * sizeof(value), sizeof(value)); + return value; +} + +static inline m2TreeNode ReadNode(const uint8_t* nodes, int32_t index) +{ + m2TreeNode node; + memcpy(&node, nodes + (size_t)index * sizeof(node), sizeof(node)); + return node; +} + +static inline int32_t InlineI32(const m2World* world, const uint8_t* in, const void* field) +{ + return ReadI32(m2StateIncoming(world, in, field), 0); +} + +// The incoming copy of a field, in a function that names the world +// `world` and the block `in`; every field the checks read is snapshot +// state in every world that has its pool. Each file that uses it undefines +// it at its end. +#define M2_IN(field) m2StateIncoming(world, in, (const void*)&world->field) + +#endif // MAUL2D_SRC_SNAPSHOT_READ_H diff --git a/third_party/maul2d/src/solver.c b/third_party/maul2d/src/solver.c index f1cdeb717..e5aab34df 100644 --- a/third_party/maul2d/src/solver.c +++ b/third_party/maul2d/src/solver.c @@ -19,15 +19,16 @@ #include // A body the integrator moves this substep. -static bool Moving(const m2World* world, int32_t i) +M2_NODISCARD static bool Moving(const m2World* world, int32_t i) { return world->bodies.alive[i] != 0 && world->bodies.types[i] != (uint8_t)m2_staticBody && world->bodies.asleep[i] == 0 && world->bodies.disabled[i] == 0; } -// A body's locked linear axes hold still: their velocity is zeroed where -// the solve begins and again where the position consumes it. A locked -// rotation needs nothing here; its inverse inertia is zero. +// A body's locked axes hold still: their velocity is zeroed where the +// solve begins and again where the position consumes it. A locked +// rotation's inverse inertia is zero, so no contact, joint or torque spins +// it, but m2Body_SetAngularVelocity can, and the zero holds it then too. static void HoldLockedAxes(m2World* world, int32_t i) { uint8_t locks = world->bodies.motionLocks[i]; @@ -39,6 +40,10 @@ static void HoldLockedAxes(m2World* world, int32_t i) { world->bodies.linearVelocities[i].y = 0.0f; } + if (world->bodies.fixedRotations[i] != 0) + { + world->bodies.angularVelocities[i] = 0.0f; + } } // Forces, gravity and damping for one substep. Damping is implicit, diff --git a/third_party/maul2d/src/state_checks.c b/third_party/maul2d/src/state_checks.c index c889a949d..15e2b5360 100644 --- a/third_party/maul2d/src/state_checks.c +++ b/third_party/maul2d/src/state_checks.c @@ -23,12 +23,12 @@ static int32_t ReadInt32(const uint8_t* bytes) return value; } -static bool InRange(int64_t value, int64_t lo, int64_t hi) +M2_NODISCARD static bool InRange(int64_t value, int64_t lo, int64_t hi) { return value >= lo && value <= hi; } -static bool CheckIndices(const uint8_t* data, size_t count, int64_t lo, int64_t range) +M2_NODISCARD static bool CheckIndices(const uint8_t* data, size_t count, int64_t lo, int64_t range) { for (size_t i = 0; i < count; ++i) { @@ -40,7 +40,7 @@ static bool CheckIndices(const uint8_t* data, size_t count, int64_t lo, int64_t return true; } -static bool CheckBytes(const uint8_t* data, size_t count, uint8_t max) +M2_NODISCARD static bool CheckBytes(const uint8_t* data, size_t count, uint8_t max) { for (size_t i = 0; i < count; ++i) { @@ -52,7 +52,7 @@ static bool CheckBytes(const uint8_t* data, size_t count, uint8_t max) return true; } -static bool CheckGeometry(const uint8_t* data, size_t count) +M2_NODISCARD static bool CheckGeometry(const uint8_t* data, size_t count) { for (size_t i = 0; i < count; ++i) { @@ -69,7 +69,7 @@ static bool CheckGeometry(const uint8_t* data, size_t count) // A point count that fits, finite geometry, and the accumulated impulses // within M2_IMPULSE_LIMIT, where the solver holds them. -static bool CheckManifolds(const uint8_t* data, size_t count) +M2_NODISCARD static bool CheckManifolds(const uint8_t* data, size_t count) { for (size_t i = 0; i < count; ++i) { @@ -93,7 +93,7 @@ static bool CheckManifolds(const uint8_t* data, size_t count) return true; } -static bool CheckPairKeys(const uint8_t* data, size_t count, int64_t shapes) +M2_NODISCARD static bool CheckPairKeys(const uint8_t* data, size_t count, int64_t shapes) { for (size_t i = 0; i < count; ++i) { @@ -107,7 +107,7 @@ static bool CheckPairKeys(const uint8_t* data, size_t count, int64_t shapes) return true; } -static bool CheckTrees(const uint8_t* data, size_t bytes, int64_t nodes) +M2_NODISCARD static bool CheckTrees(const uint8_t* data, size_t bytes, int64_t nodes) { for (size_t at = 0; at + sizeof(m2DynamicTree) <= bytes; at += sizeof(m2DynamicTree)) { @@ -122,7 +122,8 @@ static bool CheckTrees(const uint8_t* data, size_t bytes, int64_t nodes) return true; } -static bool CheckTreeNodes(const uint8_t* data, size_t count, int64_t nodes, int64_t shapes) +M2_NODISCARD static bool CheckTreeNodes(const uint8_t* data, size_t count, int64_t nodes, + int64_t shapes) { for (size_t i = 0; i < count; ++i) { @@ -137,7 +138,7 @@ static bool CheckTreeNodes(const uint8_t* data, size_t count, int64_t nodes, int return true; } -static bool CheckFinite(const uint8_t* data, size_t bytes, size_t width) +M2_NODISCARD static bool CheckFinite(const uint8_t* data, size_t bytes, size_t width) { for (size_t at = 0; at + width <= bytes; at += width) { @@ -163,7 +164,7 @@ static bool CheckFinite(const uint8_t* data, size_t bytes, size_t width) } // Every float of the block passes valid. -static bool CheckFloats(const uint8_t* data, size_t bytes, bool (*valid)(float)) +M2_NODISCARD static bool CheckFloats(const uint8_t* data, size_t bytes, bool (*valid)(float)) { for (size_t at = 0; at + sizeof(float) <= bytes; at += sizeof(float)) { @@ -178,7 +179,7 @@ static bool CheckFloats(const uint8_t* data, size_t bytes, bool (*valid)(float)) } // One axis of an acceleration, through the vector's own validator. -static bool AccelerationAxisValid(float x) +M2_NODISCARD static bool AccelerationAxisValid(float x) { return m2ValidAcceleration((m2Vec2){x, 0.0f}); } @@ -187,12 +188,12 @@ static bool AccelerationAxisValid(float x) // substepCount / dt for a step m2World_Step accepts. Dividing by a power // of two is exact, so the largest a step stores rounds to exactly the // bound. -static bool InverseStepValid(float x) +M2_NODISCARD static bool InverseStepValid(float x) { return x == 0.0f || m2WithinF(x, 1.0f / M2_STEP_LIMIT, (float)M2_MAX_SUBSTEPS / M2_MIN_STEP); } -static bool CheckRange(const uint8_t* data, size_t bytes, m2Check check) +M2_NODISCARD static bool CheckRange(const uint8_t* data, size_t bytes, m2Check check) { switch (check) { @@ -214,6 +215,8 @@ static bool CheckRange(const uint8_t* data, size_t bytes, m2Check check) return CheckFloats(data, bytes, m2ValidForce); case m2_check_maxForce: return CheckFloats(data, bytes, m2ValidMaxForce); + case m2_check_sleepThreshold: + return CheckFloats(data, bytes, m2ValidSleepThreshold); case m2_check_impulse: return CheckFloats(data, bytes, m2ValidImpulse); case m2_check_breakLimit: @@ -225,7 +228,7 @@ static bool CheckRange(const uint8_t* data, size_t bytes, m2Check check) } } -static bool CheckLocal(const uint8_t* data, size_t bytes) +M2_NODISCARD static bool CheckLocal(const uint8_t* data, size_t bytes) { for (size_t at = 0; at + sizeof(float) <= bytes; at += sizeof(float)) { @@ -239,7 +242,7 @@ static bool CheckLocal(const uint8_t* data, size_t bytes) return true; } -static bool CheckTransforms(const uint8_t* data, size_t count) +M2_NODISCARD static bool CheckTransforms(const uint8_t* data, size_t count) { for (size_t i = 0; i < count; ++i) { diff --git a/third_party/maul2d/src/state_checks.h b/third_party/maul2d/src/state_checks.h index 1f2c12c2d..5ef2b4d60 100644 --- a/third_party/maul2d/src/state_checks.h +++ b/third_party/maul2d/src/state_checks.h @@ -9,6 +9,8 @@ #ifndef MAUL2D_SRC_STATE_CHECKS_H #define MAUL2D_SRC_STATE_CHECKS_H +#include "maul2d/base.h" + #include #include #include @@ -37,18 +39,19 @@ typedef enum m2Check m2_check_transform, // a finite double position and a unit float rotation // The ranges of base.h: every float of every element passes the // validator the defs, the setters and replay use for its quantity. - m2_check_speed, // m2ValidSpeed: velocities per axis, spins, speeds - m2_check_acceleration, // m2ValidAcceleration, per axis - m2_check_gravityScale, // m2ValidGravityScale - m2_check_rate, // m2ValidRate: damping, drag, a spring's hertz - m2_check_coefficient, // m2ValidCoefficient: friction, a damping ratio - m2_check_restitution, // m2ValidRestitution - m2_check_density, // m2ValidDensity - m2_check_force, // m2ValidForce: forces per axis, torques - m2_check_maxForce, // m2ValidMaxForce: a motor's budget - m2_check_impulse, // m2ValidImpulse: accumulated joint impulses - m2_check_breakLimit, // m2ValidBreakLimit - m2_check_inverseStep, // the last step's 1 / substep (InverseStepValid) + m2_check_speed, // m2ValidSpeed: velocities per axis, spins, speeds + m2_check_acceleration, // m2ValidAcceleration, per axis + m2_check_gravityScale, // m2ValidGravityScale + m2_check_rate, // m2ValidRate: damping, drag, a spring's hertz + m2_check_coefficient, // m2ValidCoefficient: friction, a damping ratio + m2_check_restitution, // m2ValidRestitution + m2_check_density, // m2ValidDensity + m2_check_force, // m2ValidForce: forces per axis, torques + m2_check_maxForce, // m2ValidMaxForce: a motor's budget + m2_check_sleepThreshold, // m2ValidSleepThreshold: a body's resting speed + m2_check_impulse, // m2ValidImpulse: accumulated joint impulses + m2_check_breakLimit, // m2ValidBreakLimit + m2_check_inverseStep, // the last step's 1 / substep (InverseStepValid) } m2Check; // The pools an array's values may index into. @@ -60,7 +63,7 @@ typedef struct m2StateBounds // Checks count elements of elementSize bytes (an inline field: one element // of the field's size) against one rule. -bool m2StateCheckElements(const uint8_t* data, size_t count, size_t elementSize, m2Check check, - m2StateBounds bounds); +M2_NODISCARD bool m2StateCheckElements(const uint8_t* data, size_t count, size_t elementSize, + m2Check check, m2StateBounds bounds); #endif // MAUL2D_SRC_STATE_CHECKS_H diff --git a/third_party/maul2d/src/world.c b/third_party/maul2d/src/world.c index 4a88a040c..0d61ae8ed 100644 --- a/third_party/maul2d/src/world.c +++ b/third_party/maul2d/src/world.c @@ -39,7 +39,7 @@ m2WorldDef m2DefaultWorldDefValues(void) def.shapeCapacity = 2048; def.jointCapacity = 256; def.particleCapacity = 0; // fluids are opt-in - def.fluidVolumeCapacity = 0; // buoyancy volumes are opt-in + def.waterVolumeCapacity = 0; // buoyancy volumes are opt-in def.particleRadius = 0.05f; def.particleDensity = 1.0f; def.particleGravityScale = 1.0f; @@ -55,22 +55,20 @@ m2WorldDef m2DefaultWorldDefValues(void) return def; } -// Checks a world def past its cookie and version. The particle radius -// floor, M2_MIN_PARTICLE_RADIUS, is four linear slops so the skin laws -// keep meaning. +// Checks a world def past its cookie and version. The particle settings +// are checked whatever the particle capacity: the world keeps them, and a +// snapshot carries the radius, which a restore compares. The particle +// radius floor, M2_MIN_PARTICLE_RADIUS, is four linear slops so the skin +// laws keep meaning. static m2Result WorldDefCheck(const m2WorldDef* def) { if (def->bodyCapacity < 1 || def->shapeCapacity < 1 || def->jointCapacity < 1 || - def->fluidVolumeCapacity < 0 || def->particleCapacity < 0 || + def->waterVolumeCapacity < 0 || def->particleCapacity < 0 || (def->allocator.alloc == NULL) != (def->allocator.free == NULL)) { return m2_errorInvalid; } m2Result result = m2CheckAcceleration(def->gravity); - if (def->particleCapacity == 0) - { - return result; - } // A particle's mass is its density over a square of 1.5 radii, the // spacing of a fill; it must be a mass a body could have. float stride = 1.5f * def->particleRadius; @@ -97,13 +95,18 @@ static m2Result WorldDefCheck(const m2WorldDef* def) // Copies the def's settings and capacities into a zeroed world. The // derived capacities are checked products, so a def too large to address -// is refused instead of wrapping. -static bool ApplyWorldDef(m2World* world, const m2WorldDef* def) +// is refused instead of wrapping. Each broadphase tree gets two nodes per +// shape slot, which no tree can run out of: a tree of L leaves holds +// 2L - 1 nodes, a live shape is at most one leaf, and a restore proves +// the same of a snapshot's trees (snapshot_check.c), so no insert fails. +M2_NODISCARD static bool ApplyWorldDef(m2World* world, const m2WorldDef* def) { int32_t shapeCap = def->shapeCapacity; int32_t particleCap = def->particleCapacity; m2Particles* p = &world->particles; - if (ckd_mul(&world->broadphase.treeNodeCapacity, 2, shapeCap) || + int32_t jointEdges = 0; // a joint's two adjacency edges, one per body + if (ckd_mul(&jointEdges, 2, def->jointCapacity) || + ckd_mul(&world->broadphase.treeNodeCapacity, 2, shapeCap) || ckd_mul(&world->contacts.pairCapacity, 8, shapeCap) || ckd_mul(&p->particlePairCapacity, 12, particleCap) || ckd_mul(&p->particleSpringCapacity, 4, particleCap) || @@ -118,7 +121,7 @@ static bool ApplyWorldDef(m2World* world, const m2WorldDef* def) world->bodies.bodyCapacity = def->bodyCapacity; world->shapes.shapeCapacity = shapeCap; world->joints.jointCapacity = def->jointCapacity; - world->volumes.fvCapacity = def->fluidVolumeCapacity; + world->volumes.wvCapacity = def->waterVolumeCapacity; world->enqueueTask = def->enqueueTask; world->finishTask = def->finishTask; world->userTaskContext = def->userTaskContext; @@ -179,13 +182,13 @@ static void SeedFreeLists(m2World* world) { world->particles.particleFreeQueue[i] = i; } - for (int32_t i = 0; i < world->volumes.fvCapacity; ++i) + for (int32_t i = 0; i < world->volumes.wvCapacity; ++i) { - world->volumes.fvFreeQueue[i] = i; + world->volumes.wvFreeQueue[i] = i; } world->joints.jointFreeCount = jointCap; world->particles.particleFreeCount = world->particles.particleCapacity; - world->volumes.fvFreeCount = world->volumes.fvCapacity; + world->volumes.wvFreeCount = world->volumes.wvCapacity; world->bodies.freeCount = cap; world->shapes.shapeFreeCount = shapeCap; world->chains.chainFreeCount = shapeCap; @@ -201,7 +204,7 @@ static M2_NOINLINE m2Result CreateWorldWorker(const m2WorldDef* def, m2World** w // Before any solver kernel runs, make sure this CPU can execute the // backend the binary was built for: a typed refusal beats a bare // illegal-instruction trap on pre-Haswell hardware. - if (m2CheckCpu(NULL) == 0) + if (!m2CheckCpu(NULL)) { return m2_errorUnsupported; } @@ -256,16 +259,28 @@ m2Result m2CreateWorld(const m2WorldDef* def, m2World** worldOut) static M2_NOINLINE void DestroyWorldWorker(m2World* world) { - if (world == NULL) + // Host code the world runs may not destroy it under its own feet. + if (m2CheckWriter(world) != m2_success) { return; } - // The frees are host code, and nothing may change a world being - // destroyed: its writers refuse from here on. + // The frees are host code, and a free hook may read the world back. + // The pools are freed from a copy, and the world first becomes an + // empty one: every capacity, extent and count 0, every pool null and + // every tree empty. A reader the hook calls finds every id stale and + // every list empty, never a pool already freed. Nothing may change a + // world being destroyed: its writers refuse from here on. + m2World pools = *world; + memset(world, 0, sizeof(*world)); + for (int32_t t = 0; t < M2_TREE_COUNT; ++t) + { + world->broadphase.trees[t].root = M2_NULL_NODE; + world->broadphase.trees[t].freeList = M2_NULL_NODE; + } + world->allocator = pools.allocator; m2EnterHostCode(world); - m2Allocator allocator = world->allocator; - m2StateFree(world); - m2Free(&allocator, world, sizeof(m2World)); + m2StateFree(&pools); + m2Free(&pools.allocator, world, sizeof(m2World)); } void m2DestroyWorld(m2World* world) @@ -302,7 +317,7 @@ static void OpenEventWindow(m2World* world) // nothing was teleported, the pipeline provably changes no state (frozen // pairs copy themselves, islands rebuild to the same roots, the solver // has no constraints), so the step can skip it bit-identically. -static bool WorldIsHibernating(const m2World* world) +M2_NODISCARD static bool WorldIsHibernating(const m2World* world) { if (world->broadphase.movedCount != 0 || world->particles.particleCount != 0) { @@ -334,7 +349,10 @@ static bool WorldIsHibernating(const m2World* world) } // Refits the proxies of every moving body whose tight box left its fat -// box: fixed body order, shape-list order within a body. +// box: fixed body order, shape-list order within a body. The step runs it +// at its end, so the trees hold every shape where the solve left it and a +// query between steps sees it, and again at its start, for anything moved +// between steps without a refit of its own. static void MoveProxies(m2World* world) { for (int32_t i = 0; i < world->bodies.maxBodyIndex; ++i) @@ -435,26 +453,30 @@ static void ClearForces(m2World* world) // contacts, islands, solve, end. Diagnostics only. static void RecordProfile(m2World* world, const uint64_t marks[6]) { - world->profile.stepMs = (float)((double)(marks[5] - marks[0]) * 1.0e-6); - world->profile.pairsMs = (float)((double)(marks[1] - marks[0]) * 1.0e-6); - world->profile.contactsMs = (float)((double)(marks[2] - marks[1]) * 1.0e-6); - world->profile.solveMs = (float)((double)(marks[4] - marks[3]) * 1.0e-6); - world->profile.sleepMs = + world->profile.step = (float)((double)(marks[5] - marks[0]) * 1.0e-6); + world->profile.broadphase = (float)((double)(marks[1] - marks[0]) * 1.0e-6); + world->profile.narrowphase = (float)((double)(marks[2] - marks[1]) * 1.0e-6); + world->profile.solve = (float)((double)(marks[4] - marks[3]) * 1.0e-6); + world->profile.sleep = (float)((double)(marks[3] - marks[2]) * 1.0e-6 + (double)(marks[5] - marks[4]) * 1.0e-6); } static void StepWorld(m2World* world, float dt, int32_t substepCount); -static M2_NOINLINE void WorldStepWorker(m2World* world, float dt, int32_t substepCount) +static M2_NOINLINE m2Result WorldStepWorker(m2World* world, float dt, int32_t substepCount) { - if (m2CheckWriter(world) != m2_success) + m2Result writer = m2CheckWriter(world); + if (writer != m2_success) { - return; + return writer; } - if (!m2ValidTimeStep(dt) || substepCount < 1 || substepCount > M2_MAX_SUBSTEPS) + // At least one substep; the cap is a published bound. + m2Result checked = m2BothChecks( + m2CheckTimeStep(dt), + m2BothChecks(m2CheckValid(substepCount >= 1), m2CheckCount(substepCount, M2_MAX_SUBSTEPS))); + if (checked != m2_success) { - m2Refuse(world, m2_errorInvalid); - return; + return m2Refuse(world, checked); } if (world->recorder.journalActive != 0) { @@ -469,13 +491,15 @@ static M2_NOINLINE void WorldStepWorker(m2World* world, float dt, int32_t subste m2EnterHostCode(world); StepWorld(world, dt, substepCount); m2LeaveHostCode(world); + return m2_success; } -void m2World_Step(m2World* world, float dt, int32_t substepCount) +m2Result m2World_Step(m2World* world, float dt, int32_t substepCount) { uint64_t env = m2FloatEnvEnter(); - WorldStepWorker(world, dt, substepCount); + m2Result result = WorldStepWorker(world, dt, substepCount); m2FloatEnvLeave(env); + return result; } // The step itself, after the arguments are checked and the step recorded. @@ -487,11 +511,11 @@ static void StepWorld(m2World* world, float dt, int32_t substepCount) uint64_t tStart = m2TimeNowNs(); if (WorldIsHibernating(world)) { - world->profile.stepMs = (float)((double)(m2TimeNowNs() - tStart) * 1.0e-6); - world->profile.pairsMs = 0.0f; - world->profile.contactsMs = 0.0f; - world->profile.solveMs = 0.0f; - world->profile.sleepMs = 0.0f; + world->profile.step = (float)((double)(m2TimeNowNs() - tStart) * 1.0e-6); + world->profile.broadphase = 0.0f; + world->profile.narrowphase = 0.0f; + world->profile.solve = 0.0f; + world->profile.sleep = 0.0f; world->stepCount += 1; return; } @@ -505,6 +529,7 @@ static void StepWorld(m2World* world, float dt, int32_t substepCount) m2UpdatePairs(world); uint64_t tPairs = m2TimeNowNs(); m2UpdateContacts(world); + m2ClearMoved(world); uint64_t tContacts = m2TimeNowNs(); EmitTouchEvents(world); @@ -516,11 +541,11 @@ static void StepWorld(m2World* world, float dt, int32_t substepCount) m2SolveParticles(world, dt); AgeParticles(world, dt); } - if (world->volumes.maxFvIndex > 0) + if (world->volumes.maxWvIndex > 0) { // Buoyancy feeds the force accumulators before the solve, so it // integrates alongside gravity and dies with the step. - m2ApplyFluidVolumes(world, dt); + m2ApplyWaterVolumes(world, dt); } if (world->windLinearDrag > 0.0f) { @@ -533,6 +558,7 @@ static void StepWorld(m2World* world, float dt, int32_t substepCount) m2SolveStep(world, dt, substepCount); uint64_t tSolve = m2TimeNowNs(); m2UpdateSleep(world, dt); + MoveProxies(world); #ifdef MAUL2D_VALIDATE // The validate build walks the invariants after every step. M2_ASSERT(m2World_Validate(world)); @@ -792,40 +818,44 @@ m2Counters m2World_GetCounters(const m2World* world) { continue; } - counters.bodies += 1; - counters.awakeBodies += world->bodies.asleep[i] == 0 ? 1 : 0; + counters.bodyCount += 1; + // Static and kinematic bodies never sleep, so only an enabled + // dynamic body's sleep counts, as in maul3d. + bool awake = world->bodies.types[i] == (uint8_t)m2_dynamicBody && + world->bodies.disabled[i] == 0 && world->bodies.asleep[i] == 0; + counters.awakeCount += awake ? 1 : 0; } for (int32_t i = 0; i < world->shapes.maxShapeIndex; ++i) { - counters.shapes += world->shapes.shapeAlive[i] != 0 ? 1 : 0; + counters.shapeCount += world->shapes.shapeAlive[i] != 0 ? 1 : 0; } for (int32_t i = 0; i < world->joints.maxJointIndex; ++i) { - counters.joints += world->joints.jointAlive[i] != 0 ? 1 : 0; + counters.jointCount += world->joints.jointAlive[i] != 0 ? 1 : 0; } - counters.pairs = world->contacts.pairCount; + counters.contactCount = world->contacts.pairCount; for (int32_t i = 0; i < world->contacts.pairCount; ++i) { - counters.touchingPairs += world->contacts.pairTouching[i] != 0 ? 1 : 0; + counters.touchingCount += world->contacts.pairTouching[i] != 0 ? 1 : 0; } - counters.constraints = world->solver.lastConstraintCount; - counters.graphColors = world->solver.lastGraphColors; - counters.overflowConstraints = world->solver.lastOverflow; + counters.constraintCount = world->solver.lastConstraintCount; + counters.colorCount = world->solver.lastGraphColors; + counters.overflowCount = world->solver.lastOverflow; counters.stepCount = world->stepCount; - counters.pairOverflow = world->contacts.pairOverflow; - counters.particlePairOverflow = world->particles.particlePairOverflow; - counters.particleBodyOverflow = world->particles.particleBodyOverflow; - counters.particlePoolFull = world->particles.particlePoolFullCount; - counters.misuse = m2MisuseCount(world); + counters.pairsDropped = world->contacts.pairsDropped; + counters.particlePairsDropped = world->particles.particlePairsDropped; + counters.particleBodyContactsDropped = world->particles.particleBodyContactsDropped; + counters.particleCreatesRefused = world->particles.particleCreatesRefused; + counters.misuseCount = m2MisuseCount(world); return counters; } static M2_NOINLINE int32_t WorldGetBodiesWorker(const m2World* world, m2BodyId* ids, int32_t capacity) { - if (world == NULL) + if (world == NULL || !m2ListOutputValid(ids, capacity)) { - return 0; + return m2RefuseList(world, ids, capacity, sizeof(*ids)); } int32_t total = 0; for (int32_t i = 0; i < world->bodies.maxBodyIndex; ++i) @@ -844,19 +874,19 @@ static M2_NOINLINE int32_t WorldGetBodiesWorker(const m2World* world, m2BodyId* return total; } -int32_t m2World_GetBodies(const m2World* world, m2BodyId* ids, int32_t capacity) +int32_t m2World_GetBodies(const m2World* world, m2BodyId* idsOut, int32_t capacity) { uint64_t env = m2FloatEnvEnter(); - int32_t result = WorldGetBodiesWorker(world, ids, capacity); + int32_t result = WorldGetBodiesWorker(world, idsOut, capacity); m2FloatEnvLeave(env); return result; } -int32_t m2World_GetJoints(const m2World* world, m2JointId* ids, int32_t capacity) +int32_t m2World_GetJoints(const m2World* world, m2JointId* idsOut, int32_t capacity) { - if (world == NULL) + if (world == NULL || !m2ListOutputValid(idsOut, capacity)) { - return 0; + return m2RefuseList(world, idsOut, capacity, sizeof(*idsOut)); } int32_t total = 0; for (int32_t i = 0; i < world->joints.maxJointIndex; ++i) @@ -865,21 +895,21 @@ int32_t m2World_GetJoints(const m2World* world, m2JointId* ids, int32_t capacity { continue; } - if (ids != NULL && total < capacity) + if (idsOut != NULL && total < capacity) { m2JointId id = {(uint32_t)(i + 1), world->joints.jointGenerations[i]}; - ids[total] = id; + idsOut[total] = id; } total += 1; } return total; } -int32_t m2World_GetChains(const m2World* world, m2ChainId* ids, int32_t capacity) +int32_t m2World_GetChains(const m2World* world, m2ChainId* idsOut, int32_t capacity) { - if (world == NULL) + if (world == NULL || !m2ListOutputValid(idsOut, capacity)) { - return 0; + return m2RefuseList(world, idsOut, capacity, sizeof(*idsOut)); } int32_t total = 0; for (int32_t i = 0; i < world->chains.maxChainIndex; ++i) @@ -888,10 +918,10 @@ int32_t m2World_GetChains(const m2World* world, m2ChainId* ids, int32_t capacity { continue; } - if (ids != NULL && total < capacity) + if (idsOut != NULL && total < capacity) { m2ChainId id = {(uint32_t)(i + 1), world->chains.chainGenerations[i]}; - ids[total] = id; + idsOut[total] = id; } total += 1; } diff --git a/third_party/maul2d/src/world_hash.c b/third_party/maul2d/src/world_hash.c index 88c566298..df8d74ce3 100644 --- a/third_party/maul2d/src/world_hash.c +++ b/third_party/maul2d/src/world_hash.c @@ -27,13 +27,18 @@ static uint64_t HashPool(const m2HashPool* pool, uint64_t h) h = m2Hash64(h, &pool->maxIndex, (int32_t)sizeof(int32_t)); h = m2Hash64(h, &pool->count, (int32_t)sizeof(int32_t)); h = m2Hash64(h, &pool->retired, (int32_t)sizeof(int32_t)); + // head + k reaches twice the capacity, which no checked product bounds + // for bodies or water volumes, so the sum is taken in 64 bits. for (int32_t k = 0; k < pool->count; ++k) { - h = m2Hash64(h, &pool->queue[(pool->head + k) % pool->capacity], (int32_t)sizeof(int32_t)); + h = m2Hash64(h, &pool->queue[((int64_t)pool->head + k) % pool->capacity], + (int32_t)sizeof(int32_t)); } // Generations below the high-water mark, live and free: a freed slot's // generation is the one its next object takes. - return m2Hash64(h, pool->generations, pool->maxIndex * (int32_t)sizeof(uint32_t)); + // The product is taken in 64 bits, since four bytes a slot overflow 32 + // bits past 536,870,911 slots. + return m2Hash64(h, pool->generations, (int64_t)pool->maxIndex * (int64_t)sizeof(uint32_t)); } uint64_t m2HashPools(const m2World* world, m2HashPart part, uint64_t h) @@ -43,7 +48,7 @@ uint64_t m2HashPools(const m2World* world, m2HashPart part, uint64_t h) const m2Chains* c = &world->chains; const m2Joints* j = &world->joints; const m2Particles* p = &world->particles; - const m2FluidVolumes* v = &world->volumes; + const m2WaterVolumes* v = &world->volumes; const m2HashPool pools[] = { {b->freeQueue, b->generations, b->bodyCapacity, b->freeHead, b->freeCount, b->maxBodyIndex, b->retiredCount, m2_hashBodies}, @@ -55,8 +60,8 @@ uint64_t m2HashPools(const m2World* world, m2HashPart part, uint64_t h) j->jointFreeCount, j->maxJointIndex, j->jointRetiredCount, m2_hashJoints}, {p->particleFreeQueue, p->particleGenerations, p->particleCapacity, p->particleFreeHead, p->particleFreeCount, p->maxParticleIndex, p->particleRetiredCount, m2_hashParticles}, - {v->fvFreeQueue, v->fvGenerations, v->fvCapacity, v->fvFreeHead, v->fvFreeCount, - v->maxFvIndex, v->fvRetiredCount, m2_hashVolumes}, + {v->wvFreeQueue, v->wvGenerations, v->wvCapacity, v->wvFreeHead, v->wvFreeCount, + v->maxWvIndex, v->wvRetiredCount, m2_hashVolumes}, }; for (size_t i = 0; i < sizeof(pools) / sizeof(pools[0]); ++i) { diff --git a/third_party/maul2d/src/world_hash.h b/third_party/maul2d/src/world_hash.h index 4a21a0729..3099793bb 100644 --- a/third_party/maul2d/src/world_hash.h +++ b/third_party/maul2d/src/world_hash.h @@ -18,7 +18,7 @@ typedef enum m2HashPart m2_hashContacts, // pairs and manifolds m2_hashJoints, // joints and their pool m2_hashParticles, // particles, springs and triads, and their pool - m2_hashVolumes, // fluid volumes and their pool + m2_hashVolumes, // water volumes and their pool m2_hashAll, // every part, folded in table order } m2HashPart; diff --git a/third_party/maul2d/src/world_internal.c b/third_party/maul2d/src/world_internal.c index d06db1723..a31efa26e 100644 --- a/third_party/maul2d/src/world_internal.c +++ b/third_party/maul2d/src/world_internal.c @@ -26,12 +26,46 @@ static long HostCodeDepth(const m2World* world) #endif } +int32_t m2RefuseList(const m2World* world, void* items, int32_t capacity, size_t itemSize) +{ + if (items != NULL && capacity > 0) + { + memset(items, 0, (size_t)capacity * itemSize); + } + m2Refuse(world, m2_errorInvalid); + return 0; +} + +// Whether a refusal is on a replay's own path: the tape's data at fault, +// not the caller's code. Host code the replay runs (a task hook, the +// allocator) is the caller's. +M2_NODISCARD static bool TapeDriven(const m2World* world) +{ + return world != NULL && world->replaying != 0 && HostCodeDepth(world) == 0; +} + +// A writer records itself only after its checks pass, so a tape operation +// refused on replay is a tape the recording never wrote: the replay fails +// with m2_errorFormat even where the call returns void. A fill cut short by +// a full pool is recorded as such, so a full pool alone does not fail it. +static void MarkTapeRefused(const m2World* world, m2Result reason) +{ + if (TapeDriven(world) && reason != m2_errorCapacity) + { + ((m2World*)world)->tapeRefused = 1; + } +} + +m2Result m2RefuseUncounted(const m2World* world, m2Result reason) +{ + MarkTapeRefused(world, reason); + return reason; +} + m2Result m2Refuse(const m2World* world, m2Result reason) { - // A refusal on a replay's own path is the tape's data at fault, not the - // caller's code; host code the replay runs (a task hook, the allocator) - // is the caller's and still counts. - bool tapeDriven = world != NULL && world->replaying != 0 && HostCodeDepth(world) == 0; + bool tapeDriven = TapeDriven(world); + MarkTapeRefused(world, reason); // A full pool and bytes from outside are no misuse of the API. bool misuse = world != NULL && reason != m2_errorCapacity && reason != m2_errorFormat && !tapeDriven; @@ -132,7 +166,7 @@ m2Result m2CheckStatePrefix(const void* bytes, int64_t size, uint32_t magic, uin } if (prefix.abiVersion != M2_ABI_VERSION || prefix.formatVersion != formatVersion) { - return m2_errorVersion; // written by another release + return m2_errorVersion; // written by another minor version } if (prefix.headerBytes != (uint32_t)headerBytes || size < headerBytes) { diff --git a/third_party/maul2d/src/world_internal.h b/third_party/maul2d/src/world_internal.h index c1333137d..7642bf09c 100644 --- a/third_party/maul2d/src/world_internal.h +++ b/third_party/maul2d/src/world_internal.h @@ -25,6 +25,12 @@ // Per-body motion lock bits; the angular lock is fixedRotations. #define M2_LOCK_LINEAR_X 1u #define M2_LOCK_LINEAR_Y 2u +#define M2_LOCKS (M2_LOCK_LINEAR_X | M2_LOCK_LINEAR_Y) + +// Every m2_*Particle flag; a def or a restore with any other bit is refused. +#define M2_PARTICLE_FLAGS \ + ((uint32_t)(m2_tensileParticle | m2_viscousParticle | m2_powderParticle | m2_springParticle | \ + m2_elasticParticle)) // A slot names objects under generations 0 to M2_LAST_GENERATION, then // retires, so a stale id never names a later object in it. @@ -46,6 +52,7 @@ typedef struct m2Bodies uint8_t* massOverrides; // mass data from m2Body_SetMassData, not the shapes uint8_t* motionLocks; // M2_LOCK_LINEAR_X | M2_LOCK_LINEAR_Y (snapshot state) uint8_t* sleepEnables; // 0 = this body never sleeps (snapshot state) + float* sleepThresholds; // resting speed, m/s; never 0 for a live body m2Vec2* forces; // accumulated, cleared at step end (snapshot state) uint8_t* disabled; // 1 = outside simulation, shapes proxy-less (snapshot state) int8_t* dominances; // contact priority (snapshot state) @@ -60,7 +67,7 @@ typedef struct m2Bodies uint8_t* asleep; // sleep flag (snapshot + hashed: the sleep law) float* sleepTimes; // seconds under tolerance (snapshot + hashed) uint8_t* sleepStreak; // step-ends asleep, saturating at 2 (snapshot state) - uint8_t* bullets; // isBullet flag (snapshot + hashed) + uint8_t* bullets; // enableBullet flag (snapshot + hashed) uint32_t* generations; int32_t* freeQueue; int32_t freeHead; @@ -152,6 +159,10 @@ typedef struct m2Chains uint8_t* chainAlive; int32_t* chainBody; uint8_t* chainLoop; // 1 for a closed loop; read back only, never by the step + // The material the def or the chain setters last gave every link; read + // back only, never by the step, which reads each link's own. + float* chainFriction; + float* chainRestitution; uint32_t* chainGenerations; int32_t* chainFreeQueue; int32_t chainFreeHead; @@ -202,7 +213,7 @@ typedef struct m2Particles m2Vec2* particlePairNormal; int32_t particlePairCapacity; // 12 per particle of capacity int32_t particlePairCount; - int32_t particlePairOverflow; // deterministic truncation counter + int32_t particlePairsDropped; // deterministic truncation counter float* particleWeights; // step-transient dimensionless density float* particleAccumulation; // step-transient pressure accumulator m2Vec2* particleAccumulation2; // step-transient tensile normals @@ -233,30 +244,30 @@ typedef struct m2Particles int32_t* particleBodyStageDrops; // per-particle candidates beyond 4 int32_t particleBodyCapacity; // 4 per particle of capacity int32_t particleBodyCount; - int32_t particleBodyOverflow; - uint64_t particlePoolFullCount; // cumulative quiet-full refusals + int32_t particleBodyContactsDropped; + uint64_t particleCreatesRefused; // cumulative quiet-full refusals } m2Particles; -// Fluid volumes: particle-free water regions (snapshot state). -typedef struct m2FluidVolumes +// Water volumes: particle-free water regions (snapshot state). +typedef struct m2WaterVolumes { - int32_t fvCapacity; - int32_t maxFvIndex; - m2Pos2* fvLower; - m2Pos2* fvUpper; - double* fvSurface; - float* fvDensity; - float* fvLinearDrag; - float* fvAngularDrag; - m2Vec2* fvFlow; - uint64_t* fvUserData; - uint8_t* fvAlive; - uint32_t* fvGenerations; - int32_t* fvFreeQueue; - int32_t fvFreeHead; - int32_t fvFreeCount; - int32_t fvRetiredCount; -} m2FluidVolumes; + int32_t wvCapacity; + int32_t maxWvIndex; + m2Pos2* wvLower; + m2Pos2* wvUpper; + double* wvSurface; + float* wvDensity; + float* wvLinearDrag; + float* wvAngularDrag; + m2Vec2* wvFlow; + uint64_t* wvUserData; + uint8_t* wvAlive; + uint32_t* wvGenerations; + int32_t* wvFreeQueue; + int32_t wvFreeHead; + int32_t wvFreeCount; + int32_t wvRetiredCount; +} m2WaterVolumes; // Broadphase: one tree per body type whose leaves are shape proxies, // and the move buffer the next pair update consumes. @@ -264,6 +275,7 @@ typedef struct m2Broadphase { m2DynamicTree trees[M2_TREE_COUNT]; m2TreeNode* treeNodes[M2_TREE_COUNT]; + // Per tree: two per shape slot, which no tree exhausts (ApplyWorldDef). int32_t treeNodeCapacity; int32_t* proxyIds; // per shape slot; M2_NULL_NODE when absent uint8_t* inMoved; // per shape slot dedup flag @@ -285,7 +297,7 @@ typedef struct m2Contacts int32_t oldPairCount; // step-transient uint64_t* oldPairScratch; // step-transient uint64_t* pairMergeScratch; // step-transient: the merged pair list before it lands - int32_t pairOverflow; // candidate pairs dropped by the last update (table full) + int32_t pairsDropped; // candidate pairs dropped by the last update (table full) m2Manifold* manifoldScratch; // step-transient } m2Contacts; @@ -357,7 +369,7 @@ typedef struct m2World m2Joints joints; m2Chains chains; m2Particles particles; - m2FluidVolumes volumes; + m2WaterVolumes volumes; m2Broadphase broadphase; m2Contacts contacts; m2SolverScratch solver; @@ -370,9 +382,9 @@ typedef struct m2World uint8_t* restoreMarks; // Host hooks and diagnostics (never snapshot state). - m2Allocator allocator; // the def's, or the C library's; kept for the world's life - m2EnqueueTaskFn* enqueueTask; // host executor; both NULL = - m2FinishTaskFn* finishTask; // serial; never snapshot state + m2Allocator allocator; // the def's, or the C library's; kept for the world's life + m2EnqueueTaskFunction* enqueueTask; // host executor; both NULL = + m2FinishTaskFunction* finishTask; // serial; never snapshot state void* userTaskContext; m2Profile profile; // diagnostics only (never walked/hashed) // Cumulative misuse, accessed only atomically (m2Refuse, m2MisuseCount); @@ -381,19 +393,48 @@ typedef struct m2World int64_t memoryBytes; // persistent footprint, from create volatile long busy; // host code running: a step's tasks, debug draws; atomic access only uint8_t replaying; // applying a journal tape: refusals outside host code count no misuse + uint8_t tapeRefused; // a tape operation was refused while replaying: the tape fails } m2World; // Refuses a caller's call against a world and returns the reason, so a // status function can return it directly. Misuse (an invalid argument, a // stale id, a call while the world is busy) counts one in -// m2Counters.misuse; a full pool is a runtime fact and counts nothing. +// m2Counters.misuseCount; a full pool is a runtime fact and counts nothing. // Untrusted bytes refused by a restore or a replay are not misuse either: // those paths return their status without calling this, and a value a // tape's op refuses while the world replays counts nothing. world may be // NULL, which counts nothing. m2Result m2Refuse(const m2World* world, m2Result reason); + +// A refusal that is never misuse, as a dead particle's id is not: it +// counts nothing, but on a replay's own path it fails the tape as +// m2Refuse's refusals do. +m2Result m2RefuseUncounted(const m2World* world, m2Result reason); uint64_t m2MisuseCount(const m2World* world); +// Adds drops to one of m2Counters' dropped pair or contact counts, which +// stops at INT32_MAX instead of wrapping: one step may drop more pairs than +// 32 bits count, since every candidate may meet every other. +static inline void m2CountDrops(int32_t* counter, int64_t drops) +{ + int64_t total = (int64_t)*counter + drops; + *counter = total < INT32_MAX ? (int32_t)total : INT32_MAX; +} + +// Whether a list call's array and capacity make sense: a capacity of 0 +// or more, and an array unless the capacity is 0, which measures. +M2_NODISCARD static inline bool m2ListOutputValid(const void* items, int32_t capacity) +{ + return capacity >= 0 && (items != NULL || capacity == 0); +} + +// Refuses a list call: zeroes the caller's list over its whole capacity, +// since a failed call defines none of its items, counts one misuse on a +// live world as m2Refuse does, and returns the count of 0 the call +// answers. Zeroed items are null ids. A null list or a capacity of 0 or +// less has nothing to clear. +int32_t m2RefuseList(const m2World* world, void* items, int32_t capacity, size_t itemSize); + // Brackets host code the world runs (a step's tasks, a debug draw's // callbacks); in between, the world's writers refuse. Readers may draw on // several threads at once, so the count is atomic. @@ -413,7 +454,7 @@ m2Result m2CheckWriter(const m2World* world); // The 16 bytes every state format starts with, the snapshot's and the // journal tape's alike, in the layout maul3d shares: the format's magic, -// the M2_ABI_VERSION of the release that wrote it, the format's own +// the M2_ABI_VERSION of the minor version that wrote it, the format's own // number and the size of the whole header. Little-endian like the rest. typedef struct m2StatePrefix { @@ -434,7 +475,7 @@ m2StatePrefix m2MakeStatePrefix(uint32_t magic, uint32_t formatVersion, int32_t // Whether a caller's byte count of 0 or more can describe memory on this // target, that is, whether a size_t holds it. Only a 32-bit target says no. -static inline bool m2SizeFitsMemory(int64_t bytes) +M2_NODISCARD static inline bool m2SizeFitsMemory(int64_t bytes) { #if SIZE_MAX < INT64_MAX return bytes <= (int64_t)SIZE_MAX; @@ -446,10 +487,10 @@ static inline bool m2SizeFitsMemory(int64_t bytes) // Checks the prefix of untrusted bytes, in the order every state format // shares: too short or another magic is m2_errorFormat, since the bytes -// are not this format; another release or format number is -// m2_errorVersion, since only the release that wrote them reads them; a -// header size that is not this format's, or bytes too short to hold it, -// is m2_errorFormat. Counts nothing: the bytes are untrusted. +// are not this format; another ABI version or format number is +// m2_errorVersion, since only the minor version that wrote them reads +// them; a header size that is not this format's, or bytes too short to +// hold it, is m2_errorFormat. Counts nothing: the bytes are untrusted. m2Result m2CheckStatePrefix(const void* bytes, int64_t size, uint32_t magic, uint32_t formatVersion, int32_t headerBytes); diff --git a/third_party/maul2d/src/world_state.c b/third_party/maul2d/src/world_state.c index a7bcb8cca..0f50a5c74 100644 --- a/third_party/maul2d/src/world_state.c +++ b/third_party/maul2d/src/world_state.c @@ -34,7 +34,7 @@ typedef enum m2Extent m2_extent_particleSpring, m2_extent_particleTriad, m2_extent_particleBody, - m2_extent_fluidVolume, + m2_extent_waterVolume, m2_extent_constraintBytes, m2_extent_contactBlockBytes, m2_extent_markBytes, @@ -42,7 +42,7 @@ typedef enum m2Extent #define M2_STATE_SNAPSHOT 0x01u // walked by snapshots and restores #define M2_STATE_PARTICLES 0x02u // exists only when the world has particles -#define M2_STATE_FLUIDS 0x04u // exists only when the world has fluid volumes +#define M2_STATE_FLUIDS 0x04u // exists only when the world has water volumes #define M2_STATE_POINTER 0x08u // the field points at the array; else it is inline #define M2_STATE_HASH 0x10u // folded into m2World_Hash (m2StateHash) @@ -112,6 +112,11 @@ static const m2StateArray s_state[] = { M2_STATE_INLINE_CHECKED(shapes.shapeRetiredCount, M2_STATE_SNAPSHOT, count, shape), M2_STATE_INLINE_CHECKED(broadphase.movedCount, M2_STATE_SNAPSHOT, count, shape), M2_STATE_INLINE_CHECKED(contacts.pairCount, M2_STATE_SNAPSHOT, count, pair), + // The end events of touching pairs a destroy or a disable ended + // between steps, which the next step delivers: a snapshot taken in + // between carries them, so the steps after its restore deliver them too. + M2_STATE_INLINE_CHECKED(events.pendingEndCount, M2_STATE_SNAPSHOT, count, pair), + M2_STATE_INLINE_CHECKED(events.pendingSensorEndCount, M2_STATE_SNAPSHOT, count, pair), M2_STATE_CHECKED(bodies.transforms, m2Transform, body, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH, transform, one), M2_STATE_CHECKED(bodies.linearVelocities, m2Vec2, body, 1, M2_STATE_SNAPSHOT | M2_STATE_HASH, @@ -246,6 +251,8 @@ static const m2StateArray s_state[] = { M2_STATE_ARRAY(bodies.motionLocks, uint8_t, body, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH), M2_STATE_CHECKED(bodies.sleepEnables, uint8_t, body, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH, bool, one), + M2_STATE_CHECKED(bodies.sleepThresholds, float, body, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH, + sleepThreshold, one), M2_STATE_CHECKED(bodies.forces, m2Vec2, body, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH, force, one), M2_STATE_CHECKED(bodies.torques, float, body, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH, force, one), M2_STATE_CHECKED(bodies.disabled, uint8_t, body, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH, bool, @@ -258,6 +265,8 @@ static const m2StateArray s_state[] = { M2_STATE_CHECKED(chains.chainBody, int32_t, chain, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH, indexOrNone, body), M2_STATE_CHECKED(chains.chainLoop, uint8_t, chain, 0, M2_STATE_SNAPSHOT, bool, one), + M2_STATE_CHECKED(chains.chainFriction, float, chain, 0, M2_STATE_SNAPSHOT, coefficient, one), + M2_STATE_CHECKED(chains.chainRestitution, float, chain, 0, M2_STATE_SNAPSHOT, restitution, one), M2_STATE_ARRAY(chains.chainGenerations, uint32_t, chain, 0, M2_STATE_SNAPSHOT), M2_STATE_CHECKED(chains.chainFreeQueue, int32_t, chain, 0, M2_STATE_SNAPSHOT, index, shape), M2_STATE_INLINE_CHECKED(broadphase.trees, M2_STATE_SNAPSHOT, tree, treeNode), @@ -272,6 +281,8 @@ static const m2StateArray s_state[] = { M2_STATE_ARRAY(contacts.pairTouching, uint8_t, pair, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH), M2_STATE_CHECKED(contacts.manifolds, m2Manifold, pair, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH, manifold, one), + M2_STATE_ARRAY(events.pendingEndEvents, m2ContactEndEvent, pair, 0, M2_STATE_SNAPSHOT), + M2_STATE_ARRAY(events.pendingSensorEnd, m2SensorEndEvent, pair, 0, M2_STATE_SNAPSHOT), M2_STATE_CHECKED(particles.particlePositions, m2Pos2, particle, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH | M2_STATE_PARTICLES, finite64, one), M2_STATE_CHECKED(particles.particleVelocities, m2Vec2, particle, 0, @@ -320,36 +331,36 @@ static const m2StateArray s_state[] = { M2_STATE_SNAPSHOT | M2_STATE_HASH | M2_STATE_PARTICLES, finite32, one), M2_STATE_INLINE_CHECKED(particles.particleTriadCount, M2_STATE_SNAPSHOT | M2_STATE_PARTICLES, count, particleTriad), - M2_STATE_CHECKED(volumes.fvLower, m2Pos2, fluidVolume, 0, + M2_STATE_CHECKED(volumes.wvLower, m2Pos2, waterVolume, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH | M2_STATE_FLUIDS, finite64, one), - M2_STATE_CHECKED(volumes.fvUpper, m2Pos2, fluidVolume, 0, + M2_STATE_CHECKED(volumes.wvUpper, m2Pos2, waterVolume, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH | M2_STATE_FLUIDS, finite64, one), - M2_STATE_CHECKED(volumes.fvSurface, double, fluidVolume, 0, + M2_STATE_CHECKED(volumes.wvSurface, double, waterVolume, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH | M2_STATE_FLUIDS, finite64, one), - M2_STATE_CHECKED(volumes.fvDensity, float, fluidVolume, 0, + M2_STATE_CHECKED(volumes.wvDensity, float, waterVolume, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH | M2_STATE_FLUIDS, density, one), - M2_STATE_CHECKED(volumes.fvLinearDrag, float, fluidVolume, 0, + M2_STATE_CHECKED(volumes.wvLinearDrag, float, waterVolume, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH | M2_STATE_FLUIDS, rate, one), - M2_STATE_CHECKED(volumes.fvAngularDrag, float, fluidVolume, 0, + M2_STATE_CHECKED(volumes.wvAngularDrag, float, waterVolume, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH | M2_STATE_FLUIDS, rate, one), - M2_STATE_CHECKED(volumes.fvFlow, m2Vec2, fluidVolume, 0, + M2_STATE_CHECKED(volumes.wvFlow, m2Vec2, waterVolume, 0, M2_STATE_SNAPSHOT | M2_STATE_HASH | M2_STATE_FLUIDS, speed, one), - M2_STATE_ARRAY(volumes.fvUserData, uint64_t, fluidVolume, 0, + M2_STATE_ARRAY(volumes.wvUserData, uint64_t, waterVolume, 0, M2_STATE_SNAPSHOT | M2_STATE_FLUIDS), - M2_STATE_CHECKED(volumes.fvAlive, uint8_t, fluidVolume, 0, M2_STATE_SNAPSHOT | M2_STATE_FLUIDS, + M2_STATE_CHECKED(volumes.wvAlive, uint8_t, waterVolume, 0, M2_STATE_SNAPSHOT | M2_STATE_FLUIDS, bool, one), - M2_STATE_ARRAY(volumes.fvGenerations, uint32_t, fluidVolume, 0, + M2_STATE_ARRAY(volumes.wvGenerations, uint32_t, waterVolume, 0, M2_STATE_SNAPSHOT | M2_STATE_FLUIDS), - M2_STATE_CHECKED(volumes.fvFreeQueue, int32_t, fluidVolume, 0, - M2_STATE_SNAPSHOT | M2_STATE_FLUIDS, index, fluidVolume), - M2_STATE_INLINE_CHECKED(volumes.fvFreeHead, M2_STATE_SNAPSHOT | M2_STATE_FLUIDS, cursor, - fluidVolume), - M2_STATE_INLINE_CHECKED(volumes.fvFreeCount, M2_STATE_SNAPSHOT | M2_STATE_FLUIDS, count, - fluidVolume), - M2_STATE_INLINE_CHECKED(volumes.fvRetiredCount, M2_STATE_SNAPSHOT | M2_STATE_FLUIDS, count, - fluidVolume), - M2_STATE_INLINE_CHECKED(volumes.maxFvIndex, M2_STATE_SNAPSHOT | M2_STATE_FLUIDS, count, - fluidVolume), + M2_STATE_CHECKED(volumes.wvFreeQueue, int32_t, waterVolume, 0, + M2_STATE_SNAPSHOT | M2_STATE_FLUIDS, index, waterVolume), + M2_STATE_INLINE_CHECKED(volumes.wvFreeHead, M2_STATE_SNAPSHOT | M2_STATE_FLUIDS, cursor, + waterVolume), + M2_STATE_INLINE_CHECKED(volumes.wvFreeCount, M2_STATE_SNAPSHOT | M2_STATE_FLUIDS, count, + waterVolume), + M2_STATE_INLINE_CHECKED(volumes.wvRetiredCount, M2_STATE_SNAPSHOT | M2_STATE_FLUIDS, count, + waterVolume), + M2_STATE_INLINE_CHECKED(volumes.maxWvIndex, M2_STATE_SNAPSHOT | M2_STATE_FLUIDS, count, + waterVolume), // Scratch and derived arrays: allocated with the world, never walked. M2_STATE_ARRAY(solver.ccdPrevPositions, m2Pos2, body, 0, 0), @@ -382,10 +393,8 @@ static const m2StateArray s_state[] = { M2_STATE_ARRAY(solver.colorOrder, int32_t, pair, 0, 0), M2_STATE_ARRAY(events.beginEvents, m2ContactBeginEvent, pair, 0, 0), M2_STATE_ARRAY(events.endEvents, m2ContactEndEvent, pair, 0, 0), - M2_STATE_ARRAY(events.pendingEndEvents, m2ContactEndEvent, pair, 0, 0), M2_STATE_ARRAY(events.sensorBeginEvents, m2SensorBeginEvent, pair, 0, 0), M2_STATE_ARRAY(events.sensorEndEvents, m2SensorEndEvent, pair, 0, 0), - M2_STATE_ARRAY(events.pendingSensorEnd, m2SensorEndEvent, pair, 0, 0), M2_STATE_ARRAY(events.jointBreakEvents, m2JointBreakEvent, joint, 0, 0), M2_STATE_ARRAY(contacts.pairScratch, uint64_t, pair, 0, 0), M2_STATE_ARRAY(contacts.oldPairScratch, uint64_t, pair, 0, 0), @@ -431,8 +440,8 @@ static size_t ExtentCount(const m2World* world, uint8_t extent) return (size_t)world->particles.particleTriadCapacity; case m2_extent_particleBody: return (size_t)world->particles.particleBodyCapacity; - case m2_extent_fluidVolume: - return (size_t)world->volumes.fvCapacity; + case m2_extent_waterVolume: + return (size_t)world->volumes.wvCapacity; case m2_extent_constraintBytes: { // Saturates instead of wrapping on 32-bit targets, so the checked @@ -448,7 +457,7 @@ static size_t ExtentCount(const m2World* world, uint8_t extent) { int32_t most = world->bodies.bodyCapacity; const int32_t others[] = {world->shapes.shapeCapacity, world->joints.jointCapacity, - world->particles.particleCapacity, world->volumes.fvCapacity}; + world->particles.particleCapacity, world->volumes.wvCapacity}; for (size_t i = 0; i < sizeof(others) / sizeof(others[0]); ++i) { most = others[i] > most ? others[i] : most; @@ -460,13 +469,13 @@ static size_t ExtentCount(const m2World* world, uint8_t extent) return 0; } -static bool Present(const m2World* world, uint8_t flags) +M2_NODISCARD static bool Present(const m2World* world, uint8_t flags) { if ((flags & M2_STATE_PARTICLES) != 0 && world->particles.particleCapacity == 0) { return false; } - if ((flags & M2_STATE_FLUIDS) != 0 && world->volumes.fvCapacity == 0) + if ((flags & M2_STATE_FLUIDS) != 0 && world->volumes.wvCapacity == 0) { return false; } @@ -480,7 +489,7 @@ static void** FieldPointer(m2World* world, const m2StateArray* array) // The bytes an allocated array takes, or false when the size does not // fit a size_t. -static bool ArrayBytes(const m2World* world, const m2StateArray* array, size_t* bytes) +M2_NODISCARD static bool ArrayBytes(const m2World* world, const m2StateArray* array, size_t* bytes) { size_t count = 0; return !ckd_add(&count, ExtentCount(world, array->extent), (size_t)array->extra) && @@ -537,7 +546,7 @@ void m2StateFree(m2World* world) } // Whether an array is snapshot state in this world. -static bool InSnapshot(const m2World* world, const m2StateArray* array) +M2_NODISCARD static bool InSnapshot(const m2World* world, const m2StateArray* array) { return (array->flags & M2_STATE_SNAPSHOT) != 0 && Present(world, array->flags); } @@ -546,7 +555,8 @@ static bool InSnapshot(const m2World* world, const m2StateArray* array) // slot of its extent; false when that does not fit a size_t. The measure // and the walk size every array here, so a world measures to exactly the // bytes its walk copies. -static bool SnapshotArrayBytes(const m2World* world, const m2StateArray* array, size_t* bytes) +M2_NODISCARD static bool SnapshotArrayBytes(const m2World* world, const m2StateArray* array, + size_t* bytes) { *bytes = array->elementSize; return (array->flags & M2_STATE_POINTER) == 0 || @@ -599,8 +609,8 @@ size_t m2StateWalk(m2World* world, uint8_t* out, const uint8_t* in, int directio return cursor; } -static bool CheckBlock(const m2World* world, const m2StateArray* array, const uint8_t* data, - size_t count) +M2_NODISCARD static bool CheckBlock(const m2World* world, const m2StateArray* array, + const uint8_t* data, size_t count) { m2StateBounds bounds = {(int64_t)ExtentCount(world, array->range), (int64_t)world->shapes.shapeCapacity}; @@ -700,8 +710,8 @@ static m2HashSlots HashSlots(const m2World* world, uint8_t extent) case m2_extent_particle: return (m2HashSlots){world->particles.particleAlive, world->particles.maxParticleIndex, m2_hashParticles}; - case m2_extent_fluidVolume: - return (m2HashSlots){world->volumes.fvAlive, world->volumes.maxFvIndex, m2_hashVolumes}; + case m2_extent_waterVolume: + return (m2HashSlots){world->volumes.wvAlive, world->volumes.maxWvIndex, m2_hashVolumes}; case m2_extent_pair: return (m2HashSlots){NULL, world->contacts.pairCount, m2_hashContacts}; case m2_extent_particleSpring: diff --git a/third_party/maul2d/src/world_state.h b/third_party/maul2d/src/world_state.h index 3b68ee5c3..3cde5a75b 100644 --- a/third_party/maul2d/src/world_state.h +++ b/third_party/maul2d/src/world_state.h @@ -16,13 +16,13 @@ // Allocates every array, sized from the capacities already set in the // world, and adds them to its memory footprint. Returns false when an // allocation is refused; m2StateFree releases whatever was allocated. -bool m2StateAllocate(m2World* world); +M2_NODISCARD bool m2StateAllocate(m2World* world); void m2StateFree(m2World* world); // The bytes of the snapshot state, from the capacities already set in the // world alone, so a def is measured before anything is allocated. Returns // false when the sum does not fit a size_t. -bool m2StateSnapshotBytes(const m2World* world, size_t* bytesOut); +M2_NODISCARD bool m2StateSnapshotBytes(const m2World* world, size_t* bytesOut); // Walks the snapshot state in byte order: direction 0 copies into out, // 1 copies from in, and any other value only measures. Returns the @@ -33,12 +33,12 @@ size_t m2StateWalk(m2World* world, uint8_t* out, const uint8_t* in, int directio // lands: every index in range, every flag a flag, every count within its // capacity, every number within its range. The block must be exactly the // walk's size. -bool m2StateValidate(const m2World* world, const uint8_t* in); +M2_NODISCARD bool m2StateValidate(const m2World* world, const uint8_t* in); // Checks one body slot against the same rules a restore applies, so a // creation that checked its def can assert it wrote a slot a snapshot of // the world will restore. -bool m2StateBodyValid(const m2World* world, int32_t index); +M2_NODISCARD bool m2StateBodyValid(const m2World* world, int32_t index); // Where an incoming snapshot block, already accepted by m2StateValidate, // holds the bytes of one state field (a world member such as diff --git a/third_party/maul2d/src/world_validate.c b/third_party/maul2d/src/world_validate.c index cc735f7f7..e0acba25a 100644 --- a/third_party/maul2d/src/world_validate.c +++ b/third_party/maul2d/src/world_validate.c @@ -21,7 +21,7 @@ } \ } while (0) -static bool ValidBody(const m2World* world, int32_t i) +M2_NODISCARD static bool ValidBody(const m2World* world, int32_t i) { m2Transform xf = world->bodies.transforms[i]; M2_CHECK_INVARIANT(m2FinitePos2(xf.p)); @@ -33,7 +33,7 @@ static bool ValidBody(const m2World* world, int32_t i) return true; } -static bool ValidBodies(const m2World* world) +M2_NODISCARD static bool ValidBodies(const m2World* world) { for (int32_t i = 0; i < world->bodies.maxBodyIndex; ++i) { @@ -42,12 +42,12 @@ static bool ValidBodies(const m2World* world) return true; } -static bool LiveBody(const m2World* world, int32_t body) +M2_NODISCARD static bool LiveBody(const m2World* world, int32_t body) { return body >= 0 && body < world->bodies.bodyCapacity && world->bodies.alive[body] != 0; } -static bool ValidJoint(const m2World* world, int32_t i) +M2_NODISCARD static bool ValidJoint(const m2World* world, int32_t i) { M2_CHECK_INVARIANT(world->joints.jointType[i] <= 10); M2_CHECK_INVARIANT(LiveBody(world, world->joints.jointBodyA[i])); @@ -56,7 +56,7 @@ static bool ValidJoint(const m2World* world, int32_t i) } // Shapes and joints point at live bodies; contact pairs stay in key order. -static bool ValidLinks(const m2World* world) +M2_NODISCARD static bool ValidLinks(const m2World* world) { for (int32_t i = 0; i < world->shapes.maxShapeIndex; ++i) { @@ -76,7 +76,7 @@ static bool ValidLinks(const m2World* world) } // Springs and triads join live particles only. -static bool ValidParticleLinks(const m2World* world) +M2_NODISCARD static bool ValidParticleLinks(const m2World* world) { const uint8_t* alive = world->particles.particleAlive; for (int32_t k = 0; k < world->particles.particleSpringCount; ++k) @@ -93,7 +93,7 @@ static bool ValidParticleLinks(const m2World* world) return true; } -static bool ValidParticles(const m2World* world) +M2_NODISCARD static bool ValidParticles(const m2World* world) { if (world->particles.particleCapacity == 0) { @@ -118,7 +118,7 @@ static bool ValidParticles(const m2World* world) // The invariant walk: everything a healthy world must be able to // say about itself, checked loudly. Pure reader. -static M2_NOINLINE bool WorldValidateWorker(const m2World* world) +M2_NODISCARD static M2_NOINLINE bool WorldValidateWorker(const m2World* world) { if (world == NULL) { From 2342a0be9eaf1f8c7035da2eef10543ff2bcdad5 Mon Sep 17 00:00:00 2001 From: Sirac Ozmen Date: Thu, 5 Nov 2026 14:33:00 +0300 Subject: [PATCH 2/2] style(physics): format the tick refusal test --- modules/physics2d/tests/physics_test.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/physics2d/tests/physics_test.cpp b/modules/physics2d/tests/physics_test.cpp index b6f273418..cab2fe2a7 100644 --- a/modules/physics2d/tests/physics_test.cpp +++ b/modules/physics2d/tests/physics_test.cpp @@ -543,8 +543,7 @@ RAWFRAME_TEST(SettingsAndWorldsOutOfRangeAreRefused) { 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 && + 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);