Skip to content

Commit 3e34d63

Browse files
Fix Rapier initialize hard-crash when launcher stdout pipe closes
Closing CurseForge/Overwolf while Minecraft stays open breaks the process stdout pipe. fern then panics on info! during Rapier3D.initialize (Windows os error 232), which previously crossed JNI and killed the JVM with hs_err. Ignore closed-pipe writes in the native logger and catch_unwind initialize like tick/step so failures become Java exceptions / crash reports.
1 parent 550e644 commit 3e34d63

2 files changed

Lines changed: 153 additions & 99 deletions

File tree

sable_rapier/src/main/java/dev/ryanhcode/sable/physics/impl/rapier/RapierPhysicsPipeline.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,19 @@ public void init(@Nullable final Vector3dc gravity, final double universalDrag)
121121
this.scene = new RapierPhysicsScene(Rapier3D.initialize(gravity.x(), gravity.y(), gravity.z(), universalDrag));
122122
} catch (final UnsatisfiedLinkError e) {
123123
Sable.LOGGER.error("Sable has failed to link with the natives for its Rapier pipeline. Please report with system details to " + Sable.ISSUE_TRACKER_URL, e);
124-
final CrashReport crashReport = CrashReport.forThrowable(e.getCause(), "Sable linking with Rapier natives");
124+
final CrashReport crashReport = CrashReport.forThrowable(e.getCause() != null ? e.getCause() : e, "Sable linking with Rapier natives");
125125
final CrashReportCategory category = crashReport.addCategory("Natives");
126126
category.setDetail("Name", Rapier3D.NATIVE_NAME);
127127
throw new ReportedException(crashReport);
128+
} catch (final RuntimeException e) {
129+
// Native panics in initialize are converted to RuntimeException (see sable_rapier JNI catch_unwind).
130+
// Previously these killed the JVM with hs_err and no crash-report.
131+
Sable.LOGGER.error("Sable Rapier physics scene failed to initialize. Please report with system details to " + Sable.ISSUE_TRACKER_URL, e);
132+
final CrashReport crashReport = CrashReport.forThrowable(e, "Sable initializing Rapier physics scene");
133+
final CrashReportCategory category = crashReport.addCategory("Natives");
134+
category.setDetail("Name", Rapier3D.NATIVE_NAME);
135+
category.setDetail("Note", "If CurseForge/Overwolf was closed while Minecraft stayed open, fully quit the game and relaunch via the launcher before reporting.");
136+
throw new ReportedException(crashReport);
128137
}
129138
}
130139

sable_rapier/src/main/rust/rapier/src/lib.rs

Lines changed: 143 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use jni::sys::{jboolean, jdouble, jint, jlong};
1818
use jni::{JNIEnv, JavaVM};
1919
use rapier3d::glamx::{DVec3, Quat};
2020
use std::collections::HashMap;
21+
use std::io::{self, Write};
2122
use std::sync::{Arc, OnceLock, RwLock};
2223

2324
use fern::colors::{Color, ColoredLevelConfig};
@@ -303,122 +304,166 @@ pub fn get_rigid_body<'a>(
303304
&sim.rigid_body_set[*handle]
304305
}
305306

307+
/// Stdout/stderr attached to CurseForge/Overwolf become broken pipes when the launcher is closed
308+
/// while Minecraft stays open. fern → log then panics with os error 232, aborting initialize even
309+
/// though the physics scene was already constructed. Treat closed-pipe writes as success.
310+
struct IgnoreClosedPipe<W>(W);
311+
312+
impl<W: Write> Write for IgnoreClosedPipe<W> {
313+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
314+
match self.0.write(buf) {
315+
Err(e) if is_closed_pipe(&e) => Ok(buf.len()),
316+
other => other,
317+
}
318+
}
319+
320+
fn flush(&mut self) -> io::Result<()> {
321+
match self.0.flush() {
322+
Err(e) if is_closed_pipe(&e) => Ok(()),
323+
other => other,
324+
}
325+
}
326+
}
327+
328+
fn is_closed_pipe(err: &io::Error) -> bool {
329+
matches!(
330+
err.kind(),
331+
io::ErrorKind::BrokenPipe | io::ErrorKind::UnexpectedEof
332+
) || err.raw_os_error() == Some(232) // Windows ERROR_NO_DATA / "The pipe is being closed"
333+
}
334+
306335
#[unsafe(no_mangle)]
307336
pub extern "system" fn Java_dev_ryanhcode_sable_physics_impl_rapier_Rapier3D_initialize<'local>(
308-
env: JNIEnv<'local>,
337+
mut env: JNIEnv<'local>,
309338
_class: JClass<'local>,
310339
x: jdouble,
311340
y: jdouble,
312341
z: jdouble,
313342
universal_drag: jdouble,
314343
) -> jlong {
315-
PHYSICS_STATE.get_or_init(|| {
316-
let colors = ColoredLevelConfig::new()
317-
.info(Color::Green)
318-
.error(Color::Red)
319-
.debug(Color::Blue);
320-
321-
let _ = fern::Dispatch::new()
322-
.format(move |out, message, record| {
323-
out.finish(format_args!(
324-
"[{}] [{}] ({}) {}",
325-
humantime::format_rfc3339(std::time::SystemTime::now()),
326-
colors.color(record.level()),
327-
record.target(),
328-
message
329-
))
344+
// initialize used to unwind across the JNI boundary on panic, which hard-kills the JVM
345+
// (EXCEPTION_UNCAUGHT_CXX_EXCEPTION / hs_err) with no Minecraft crash report. Mirror tick/step.
346+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
347+
PHYSICS_STATE.get_or_init(|| {
348+
let colors = ColoredLevelConfig::new()
349+
.info(Color::Green)
350+
.error(Color::Red)
351+
.debug(Color::Blue);
352+
353+
let _ = fern::Dispatch::new()
354+
.format(move |out, message, record| {
355+
out.finish(format_args!(
356+
"[{}] [{}] ({}) {}",
357+
humantime::format_rfc3339(std::time::SystemTime::now()),
358+
colors.color(record.level()),
359+
record.target(),
360+
message
361+
))
362+
})
363+
.level(log::LevelFilter::Info)
364+
.level_for("jni", log::LevelFilter::Error)
365+
.chain(Box::new(IgnoreClosedPipe(io::stdout())) as Box<dyn Write + Send>)
366+
.apply();
367+
368+
RwLock::new(PhysicsState {
369+
integration_parameters: IntegrationParameters {
370+
dt: 1.0 / 20.0,
371+
372+
max_ccd_substeps: 3,
373+
normalized_prediction_distance: 0.005,
374+
375+
contact_softness: SpringCoefficients {
376+
natural_frequency: 30.0,
377+
damping_ratio: 5.0,
378+
},
379+
380+
normalized_max_corrective_velocity: 50.0,
381+
normalized_allowed_linear_error: 0.0025,
382+
383+
..IntegrationParameters::default()
384+
},
385+
voxel_collider_map: VoxelColliderMap::new(),
330386
})
331-
.level(log::LevelFilter::Info)
332-
.level_for("jni", log::LevelFilter::Error)
333-
.chain(std::io::stdout())
334-
.apply();
335-
336-
RwLock::new(PhysicsState {
337-
integration_parameters: IntegrationParameters {
338-
dt: 1.0 / 20.0,
387+
});
339388

340-
max_ccd_substeps: 3,
341-
normalized_prediction_distance: 0.005,
389+
let ground = RigidBodyBuilder::fixed();
390+
391+
let collider = ColliderBuilder::new(SharedShape::new(LevelCollider::new(None, true)))
392+
.collision_groups(LEVEL_GROUP)
393+
.build();
394+
395+
let sable_data = Arc::new(RwLock::new(SableSceneData {
396+
main_level_chunks: HashMap::<i64, ChunkSection>::new(),
397+
octree_chunks: HashMap::<i64, OctreeChunkSection>::new(),
398+
joint_set: SableJointSet::new(),
399+
rope_map: RopeMap::default(),
400+
level_colliders: HashMap::<LevelColliderID, ActiveLevelColliderInfo>::new(),
401+
rigid_bodies: HashMap::<LevelColliderID, RigidBodyHandle>::new(),
402+
}));
403+
let manifold_info_map = Arc::new(SableManifoldInfoMap::default());
404+
let reported_collisions = Arc::new(ReportedCollisionBuffer::new());
405+
let current_step_vm = Some(Arc::new(unsafe {
406+
JavaVM::from_raw(env.get_java_vm().unwrap().get_java_vm_pointer()).unwrap()
407+
}));
408+
409+
let dispatcher = SableDispatcher {
410+
sable_data: Arc::clone(&sable_data),
411+
manifold_info_map: Arc::clone(&manifold_info_map),
412+
};
342413

343-
contact_softness: SpringCoefficients {
344-
natural_frequency: 30.0,
345-
damping_ratio: 5.0,
414+
let mut scene = PhysicsScene {
415+
sim_data: RwLock::new(SimulationSceneData {
416+
pipeline: PhysicsPipeline::new(),
417+
rigid_body_set: RigidBodySet::new(),
418+
collider_set: ColliderSet::new(),
419+
island_manager: IslandManager::new(),
420+
broad_phase: DefaultBroadPhase::new(),
421+
narrow_phase: NarrowPhase::with_query_dispatcher(
422+
dispatcher.chain(DefaultQueryDispatcher),
423+
),
424+
impulse_joint_set: ImpulseJointSet::new(),
425+
multibody_joint_set: MultibodyJointSet::new(),
426+
ccd_solver: CCDSolver::new(),
427+
physics_hooks: SablePhysicsHooks {
428+
sable_data: Arc::clone(&sable_data),
429+
manifold_info_map: Arc::clone(&manifold_info_map),
430+
current_step_vm: current_step_vm.clone(),
346431
},
432+
event_handler: SableEventHandler {
433+
reported_collisions: Arc::clone(&reported_collisions),
434+
},
435+
}),
436+
sable_data,
437+
ground_handle: None,
438+
reported_collisions,
439+
current_step_vm,
440+
gravity: Vec3::new(x as Real, y as Real, z as Real),
441+
universal_drag: universal_drag as Real,
442+
manifold_info_map,
443+
};
347444

348-
normalized_max_corrective_velocity: 50.0,
349-
normalized_allowed_linear_error: 0.0025,
350-
351-
..IntegrationParameters::default()
352-
},
353-
voxel_collider_map: VoxelColliderMap::new(),
354-
})
355-
});
356-
357-
let ground = RigidBodyBuilder::fixed();
445+
{
446+
let mut sim_data = scene.sim_data.write().unwrap();
447+
sim_data.collider_set.insert(collider);
358448

359-
let collider = ColliderBuilder::new(SharedShape::new(LevelCollider::new(None, true)))
360-
.collision_groups(LEVEL_GROUP)
361-
.build();
449+
scene.ground_handle = Some(sim_data.rigid_body_set.insert(ground));
450+
}
362451

363-
let sable_data = Arc::new(RwLock::new(SableSceneData {
364-
main_level_chunks: HashMap::<i64, ChunkSection>::new(),
365-
octree_chunks: HashMap::<i64, OctreeChunkSection>::new(),
366-
joint_set: SableJointSet::new(),
367-
rope_map: RopeMap::default(),
368-
level_colliders: HashMap::<LevelColliderID, ActiveLevelColliderInfo>::new(),
369-
rigid_bodies: HashMap::<LevelColliderID, RigidBodyHandle>::new(),
370-
}));
371-
let manifold_info_map = Arc::new(SableManifoldInfoMap::default());
372-
let reported_collisions = Arc::new(ReportedCollisionBuffer::new());
373-
let current_step_vm = Some(Arc::new(unsafe {
374-
JavaVM::from_raw(env.get_java_vm().unwrap().get_java_vm_pointer()).unwrap()
452+
info!("Rapier scene initialized");
453+
Arc::into_raw(Arc::new(scene)) as jlong
375454
}));
376455

377-
let dispatcher = SableDispatcher {
378-
sable_data: Arc::clone(&sable_data),
379-
manifold_info_map: Arc::clone(&manifold_info_map),
380-
};
381-
382-
let mut scene = PhysicsScene {
383-
sim_data: RwLock::new(SimulationSceneData {
384-
pipeline: PhysicsPipeline::new(),
385-
rigid_body_set: RigidBodySet::new(),
386-
collider_set: ColliderSet::new(),
387-
island_manager: IslandManager::new(),
388-
broad_phase: DefaultBroadPhase::new(),
389-
narrow_phase: NarrowPhase::with_query_dispatcher(
390-
dispatcher.chain(DefaultQueryDispatcher),
391-
),
392-
impulse_joint_set: ImpulseJointSet::new(),
393-
multibody_joint_set: MultibodyJointSet::new(),
394-
ccd_solver: CCDSolver::new(),
395-
physics_hooks: SablePhysicsHooks {
396-
sable_data: Arc::clone(&sable_data),
397-
manifold_info_map: Arc::clone(&manifold_info_map),
398-
current_step_vm: current_step_vm.clone(),
399-
},
400-
event_handler: SableEventHandler {
401-
reported_collisions: Arc::clone(&reported_collisions),
402-
},
403-
}),
404-
sable_data,
405-
ground_handle: None,
406-
reported_collisions,
407-
current_step_vm,
408-
gravity: Vec3::new(x as Real, y as Real, z as Real),
409-
universal_drag: universal_drag as Real,
410-
manifold_info_map,
411-
};
412-
413-
{
414-
let mut sim_data = scene.sim_data.write().unwrap();
415-
sim_data.collider_set.insert(collider);
416-
417-
scene.ground_handle = Some(sim_data.rigid_body_set.insert(ground));
456+
match result {
457+
Ok(handle) => handle,
458+
Err(payload) => {
459+
let msg = format!(
460+
"Rapier native panic during initialize: {}",
461+
panic_message(&payload)
462+
);
463+
let _ = env.throw_new("java/lang/RuntimeException", &msg);
464+
0
465+
}
418466
}
419-
420-
info!("Rapier scene initialized");
421-
Arc::into_raw(Arc::new(scene)) as jlong
422467
}
423468

424469
#[unsafe(no_mangle)]

0 commit comments

Comments
 (0)