Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

2D: RuntimeError "unreachable" in World.step() when a ball settles against ConvexPolygon bodies (deterministic repro, 25/25 crashes)

Đang mở
#985 3 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

@Bardakor đang làm issue này rồi.

Từ ngày 16/8/2026.

  • #996 của @Bardakor — đang mở

Đánh giá

Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức phù hợp với người mới
45/100
Loại issue
Lỗi
Độ rõ ràng
Khá rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
javascript, rust, wasm
Lĩnh vực
game-dev

Hướng nghiên cứu

Bắt đầu bằng cách chạy repro.cjs được cung cấp với @dimforge/rapier2d-compat ở các chế độ sharp, round002 và round008, sau đó kiểm tra đường đi World.step() được chỉ ra bởi wasm stack. So sánh các trường hợp ConvexPolygon với trường hợp đối chứng cuboid và chuỗi phân mảnh; được xem là hoàn tất khi các biến thể xác định hoàn thành mà không có RuntimeError, đồng thời các polygon collider vẫn được hỗ trợ.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Environment

Package @dimforge/rapier2d-compat
Versions reproduced 0.20.0 (latest stable) and canary 0.0.0-5de07a4-20260808 (different wasm hash, identical panic ticks)
Runtime Node.js v24.15.0, Windows 11 x64, single-threaded
Settings All defaults except timestep = 1/60, lengthUnit = 1.0, numSolverIterations = 4, gravity (0, -20)

Summary

World.step() panics with RuntimeError: unreachable (uncatchable — the world is unusable afterwards) when a ball rests on / grinds against ConvexPolygon bodies in a small settling pile. The panic occurs tens of ticks after the initial impact, while the pile is settling.

The repro below is ~100 lines of plain rapier calls, fully deterministic, no external assets. It runs 25 parameter variants (ball density ×0.8–1.2, five drop positions) per shape mode:

Shape mode Crashes
convexHull (sharp) 25/25 (panic at tick 25–53)
roundConvexHull, radius 0.02 24/25 (tick 36–80)
roundConvexHull, radius 0.08 25/25 (tick 42–73)
same silhouette approximated with cuboids 0/25

Reproduction

npm i @dimforge/[email protected], save as repro.cjs, then node repro.cjs sharp (or round002 / round008):

repro.cjs
// Standalone repro: `unreachable` panic in rapier2d-compat 0.20.0 narrow phase.
// Run: node tmp-repro.cjs [sharp|round002|round008]
const RAPIER = require('@dimforge/rapier2d-compat');

const FOOT_L = [-1.5, 0, -1.1, 0, -0.55, 1.4, -0.95, 1.4];
const FOOT_R = [1.5, 0, 1.1, 0, 0.55, 1.4, 0.95, 1.4];
const LINTEL = [-1.2, 1.4, 1.2, 1.4, 1.2, 2.0, -1.2, 2.0];
const PARTS = [FOOT_L, FOOT_R, LINTEL];
const RELEASE = [
  { vx: 3, w: -1.5 },
  { vx: -3, w: 1.5 },
  { vx: 0, w: 0 },
];

function shrink(points, margin) {
  const n = points.length / 2;
  let cx = 0, cy = 0;
  for (let i = 0; i < n; i += 1) { cx += points[i * 2]; cy += points[i * 2 + 1]; }
  cx /= n; cy /= n;
  const out = [];
  for (let i = 0; i < n; i += 1) {
    const dx = points[i * 2] - cx, dy = points[i * 2 + 1] - cy;
    const len = Math.hypot(dx, dy);
    const k = len > margin ? (len - margin) / len : 0;
    out.push(cx + dx * k, cy + dy * k);
  }
  return out;
}

function hullDesc(points, radius) {
  const desc = radius > 0
    ? RAPIER.ColliderDesc.roundConvexHull(new Float32Array(shrink(points, radius)), radius)
    : RAPIER.ColliderDesc.convexHull(new Float32Array(points));
  return desc.setDensity(1).setFriction(0.6).setRestitution(0);
}

function run(ballDensity, hitX, radius) {
  const world = new RAPIER.World({ x: 0, y: -20 });
  world.timestep = 1 / 60;
  world.lengthUnit = 1.0;
  world.numSolverIterations = 4;
  const events = new RAPIER.EventQueue(true);

  const ground = world.createRigidBody(RAPIER.RigidBodyDesc.fixed().setTranslation(15, -0.5));
  world.createCollider(
    RAPIER.ColliderDesc.cuboid(30, 0.5).setFriction(0.6).setRestitution(0),
    ground,
  );

  // Fixed composite "arch": two slanted feet + rectangular lintel.
  const arch = world.createRigidBody(RAPIER.RigidBodyDesc.fixed().setTranslation(6, 0));
  const archColliders = PARTS.map((pts) =>
    world.createCollider(
      hullDesc(pts, radius).setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS),
      arch,
    ),
  );
  const archHandles = new Set(archColliders.map((c) => c.handle));

  const ball = world.createRigidBody(
    RAPIER.RigidBodyDesc.dynamic().setTranslation(hitX, 3.5).setLinvel(2, -12),
  );
  const ballCollider = world.createCollider(
    RAPIER.ColliderDesc.ball(0.25).setDensity(ballDensity).setRestitution(0),
    ball,
  );

  let fragmented = false;
  for (let i = 0; i < 600; i += 1) {
    const prevVx = ball.linvel().x;
    try {
      world.step(events);
    } catch (err) {
      return i; // panic tick
    }
    let hit = false;
    events.drainCollisionEvents((h1, h2, started) => {
      if (!started) return;
      const pair = [h1, h2];
      if (pair.includes(ballCollider.handle) && pair.some((h) => archHandles.has(h))) hit = true;
    });
    if (hit && !fragmented) {
      fragmented = true;
      // Replace the composite with three dynamic fragments at identical
      // world transforms; lintel gets 60 % of the ball's horizontal momentum.
      const t = arch.translation();
      const rot = arch.rotation();
      world.removeRigidBody(arch);
      const ballMomentumX = ball.mass() * prevVx;
      PARTS.forEach((pts, k) => {
        const body = world.createRigidBody(
          RAPIER.RigidBodyDesc.dynamic()
            .setTranslation(t.x, t.y)
            .setRotation(rot)
            .setLinvel(RELEASE[k].vx, 0)
            .setAngvel(RELEASE[k].w),
        );
        world.createCollider(hullDesc(pts, radius), body);
        if (k === 2) {
          body.setLinvel({ x: (0.6 * ballMomentumX) / body.mass(), y: 0 }, true);
        }
      });
    }
  }
  return null;
}

async function main() {
  await RAPIER.init();
  const mode = process.argv[2] ?? 'round002';
  const radius = mode === 'round002' ? 0.02 : mode === 'round008' ? 0.08 : 0;
  let runs = 0, panics = 0;
  const ticks = [];
  for (const dScale of [0.8, 0.9, 1.0, 1.1, 1.2]) {
    for (const hitX of [5.7, 5.9, 6.0, 6.1, 6.3]) {
      runs += 1;
      const panicTick = run(8.0 * dScale, hitX, radius);
      if (panicTick !== null) { panics += 1; ticks.push(panicTick); }
    }
  }
  console.log(`${mode}: ${panics}/${runs} panicked${ticks.length ? ` (ticks: ${ticks.join(', ')})` : ''}`);
}

main();

The scene: a fixed ground cuboid; a fixed composite body with three convexHull colliders (two slanted parallelogram "feet" and a rectangular "lintel" forming an arch); a ball (r 0.25, restitution 0) dropped onto the lintel with velocity (2, −12). On the first ball↔arch contact event the composite is replaced by three dynamic bodies with the same shapes at identical world transforms and small deterministic release velocities. The ball and the three polygon bodies then settle as a pile on the ground — and world.step() hits unreachable some 25–80 ticks in.

Stack (wasm offsets, 0.20.0 compat build):

RuntimeError: unreachable
  wasm:/wasm/005ab5b2:1:1260375
  wasm:/wasm/005ab5b2:1:1403970
  wasm:/wasm/005ab5b2:1:1404031
  wasm:/wasm/005ab5b2:1:491284
  wasm:/wasm/005ab5b2:1:221623
  wasm:/wasm/005ab5b2:1:679662
  N.stepWithEvents (rapier.mjs)

Additional observations (from a larger game harness, same scene)

  • The ball is required. Removing the ball right after the composite is fragmented: 0/25 crashes in a configuration that otherwise crashes 22/25.
  • Polygon shapes are required. With the three polygons replaced by cuboid approximations of the same silhouette we have never seen a panic (0/25 in every configuration, and none across the rest of the project's physics suites, which are cuboid/ball-only).
  • Not stiffness-related. Reproduces at the default contact_natural_frequency (the table above) and at 120 (there sharp hulls crash less often, 1/25, while round ones still crash 22/25).
  • The trigger state is not captured by snapshots. World.restoreSnapshot(world.takeSnapshot()) taken on the step before the panic does not panic when stepped; a fresh world built with the exact panic-frame poses/velocities does not panic either. The trigger seems to live in non-serialized state (contact workspace / warm-start?).
  • Identical panic ticks on the 0.20.0 stable wasm and the 2026-08-08 canary wasm.

Workaround

Model colliders as cuboids/balls only (avoiding ConvexPolygon/RoundConvexPolygon) — no crashes in any configuration.

Ngôn ngữ chính
Rust
Star
6k
Fork
421
Merge trung bình
6 giờ 11 phút
Pull request đã merge (30 ngày)
4

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của dimforge/rapier

Tất cả issue của dimforge/rapier

Issue tương tự

Thêm issue về Rust

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.