diff --git a/sable_rapier/src/main/java/dev/ryanhcode/sable/physics/impl/rapier/RapierPhysicsPipeline.java b/sable_rapier/src/main/java/dev/ryanhcode/sable/physics/impl/rapier/RapierPhysicsPipeline.java index f741a1a1..6b72b366 100644 --- a/sable_rapier/src/main/java/dev/ryanhcode/sable/physics/impl/rapier/RapierPhysicsPipeline.java +++ b/sable_rapier/src/main/java/dev/ryanhcode/sable/physics/impl/rapier/RapierPhysicsPipeline.java @@ -121,10 +121,19 @@ public void init(@Nullable final Vector3dc gravity, final double universalDrag) this.scene = new RapierPhysicsScene(Rapier3D.initialize(gravity.x(), gravity.y(), gravity.z(), universalDrag)); } catch (final UnsatisfiedLinkError e) { 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); - final CrashReport crashReport = CrashReport.forThrowable(e.getCause(), "Sable linking with Rapier natives"); + final CrashReport crashReport = CrashReport.forThrowable(e.getCause() != null ? e.getCause() : e, "Sable linking with Rapier natives"); final CrashReportCategory category = crashReport.addCategory("Natives"); category.setDetail("Name", Rapier3D.NATIVE_NAME); throw new ReportedException(crashReport); + } catch (final RuntimeException e) { + // Native panics in initialize are converted to RuntimeException (see sable_rapier JNI catch_unwind). + // Previously these killed the JVM with hs_err and no crash-report. + Sable.LOGGER.error("Sable Rapier physics scene failed to initialize. Please report with system details to " + Sable.ISSUE_TRACKER_URL, e); + final CrashReport crashReport = CrashReport.forThrowable(e, "Sable initializing Rapier physics scene"); + final CrashReportCategory category = crashReport.addCategory("Natives"); + category.setDetail("Name", Rapier3D.NATIVE_NAME); + category.setDetail("Note", "If CurseForge/Overwolf was closed while Minecraft stayed open, fully quit the game and relaunch via the launcher before reporting."); + throw new ReportedException(crashReport); } } diff --git a/sable_rapier/src/main/rust/rapier/src/lib.rs b/sable_rapier/src/main/rust/rapier/src/lib.rs index 2d224733..6ad9826f 100644 --- a/sable_rapier/src/main/rust/rapier/src/lib.rs +++ b/sable_rapier/src/main/rust/rapier/src/lib.rs @@ -18,6 +18,7 @@ use jni::sys::{jboolean, jdouble, jint, jlong}; use jni::{JNIEnv, JavaVM}; use rapier3d::glamx::{DVec3, Quat}; use std::collections::HashMap; +use std::io::{self, Write}; use std::sync::{Arc, OnceLock, RwLock}; use fern::colors::{Color, ColoredLevelConfig}; @@ -303,122 +304,166 @@ pub fn get_rigid_body<'a>( &sim.rigid_body_set[*handle] } +/// Stdout/stderr attached to CurseForge/Overwolf become broken pipes when the launcher is closed +/// while Minecraft stays open. fern → log then panics with os error 232, aborting initialize even +/// though the physics scene was already constructed. Treat closed-pipe writes as success. +struct IgnoreClosedPipe(W); + +impl Write for IgnoreClosedPipe { + fn write(&mut self, buf: &[u8]) -> io::Result { + match self.0.write(buf) { + Err(e) if is_closed_pipe(&e) => Ok(buf.len()), + other => other, + } + } + + fn flush(&mut self) -> io::Result<()> { + match self.0.flush() { + Err(e) if is_closed_pipe(&e) => Ok(()), + other => other, + } + } +} + +fn is_closed_pipe(err: &io::Error) -> bool { + matches!( + err.kind(), + io::ErrorKind::BrokenPipe | io::ErrorKind::UnexpectedEof + ) || err.raw_os_error() == Some(232) // Windows ERROR_NO_DATA / "The pipe is being closed" +} + #[unsafe(no_mangle)] pub extern "system" fn Java_dev_ryanhcode_sable_physics_impl_rapier_Rapier3D_initialize<'local>( - env: JNIEnv<'local>, + mut env: JNIEnv<'local>, _class: JClass<'local>, x: jdouble, y: jdouble, z: jdouble, universal_drag: jdouble, ) -> jlong { - PHYSICS_STATE.get_or_init(|| { - let colors = ColoredLevelConfig::new() - .info(Color::Green) - .error(Color::Red) - .debug(Color::Blue); - - let _ = fern::Dispatch::new() - .format(move |out, message, record| { - out.finish(format_args!( - "[{}] [{}] ({}) {}", - humantime::format_rfc3339(std::time::SystemTime::now()), - colors.color(record.level()), - record.target(), - message - )) + // initialize used to unwind across the JNI boundary on panic, which hard-kills the JVM + // (EXCEPTION_UNCAUGHT_CXX_EXCEPTION / hs_err) with no Minecraft crash report. Mirror tick/step. + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + PHYSICS_STATE.get_or_init(|| { + let colors = ColoredLevelConfig::new() + .info(Color::Green) + .error(Color::Red) + .debug(Color::Blue); + + let _ = fern::Dispatch::new() + .format(move |out, message, record| { + out.finish(format_args!( + "[{}] [{}] ({}) {}", + humantime::format_rfc3339(std::time::SystemTime::now()), + colors.color(record.level()), + record.target(), + message + )) + }) + .level(log::LevelFilter::Info) + .level_for("jni", log::LevelFilter::Error) + .chain(Box::new(IgnoreClosedPipe(io::stdout())) as Box) + .apply(); + + RwLock::new(PhysicsState { + integration_parameters: IntegrationParameters { + dt: 1.0 / 20.0, + + max_ccd_substeps: 3, + normalized_prediction_distance: 0.005, + + contact_softness: SpringCoefficients { + natural_frequency: 30.0, + damping_ratio: 5.0, + }, + + normalized_max_corrective_velocity: 50.0, + normalized_allowed_linear_error: 0.0025, + + ..IntegrationParameters::default() + }, + voxel_collider_map: VoxelColliderMap::new(), }) - .level(log::LevelFilter::Info) - .level_for("jni", log::LevelFilter::Error) - .chain(std::io::stdout()) - .apply(); - - RwLock::new(PhysicsState { - integration_parameters: IntegrationParameters { - dt: 1.0 / 20.0, + }); - max_ccd_substeps: 3, - normalized_prediction_distance: 0.005, + let ground = RigidBodyBuilder::fixed(); + + let collider = ColliderBuilder::new(SharedShape::new(LevelCollider::new(None, true))) + .collision_groups(LEVEL_GROUP) + .build(); + + let sable_data = Arc::new(RwLock::new(SableSceneData { + main_level_chunks: HashMap::::new(), + octree_chunks: HashMap::::new(), + joint_set: SableJointSet::new(), + rope_map: RopeMap::default(), + level_colliders: HashMap::::new(), + rigid_bodies: HashMap::::new(), + })); + let manifold_info_map = Arc::new(SableManifoldInfoMap::default()); + let reported_collisions = Arc::new(ReportedCollisionBuffer::new()); + let current_step_vm = Some(Arc::new(unsafe { + JavaVM::from_raw(env.get_java_vm().unwrap().get_java_vm_pointer()).unwrap() + })); + + let dispatcher = SableDispatcher { + sable_data: Arc::clone(&sable_data), + manifold_info_map: Arc::clone(&manifold_info_map), + }; - contact_softness: SpringCoefficients { - natural_frequency: 30.0, - damping_ratio: 5.0, + let mut scene = PhysicsScene { + sim_data: RwLock::new(SimulationSceneData { + pipeline: PhysicsPipeline::new(), + rigid_body_set: RigidBodySet::new(), + collider_set: ColliderSet::new(), + island_manager: IslandManager::new(), + broad_phase: DefaultBroadPhase::new(), + narrow_phase: NarrowPhase::with_query_dispatcher( + dispatcher.chain(DefaultQueryDispatcher), + ), + impulse_joint_set: ImpulseJointSet::new(), + multibody_joint_set: MultibodyJointSet::new(), + ccd_solver: CCDSolver::new(), + physics_hooks: SablePhysicsHooks { + sable_data: Arc::clone(&sable_data), + manifold_info_map: Arc::clone(&manifold_info_map), + current_step_vm: current_step_vm.clone(), }, + event_handler: SableEventHandler { + reported_collisions: Arc::clone(&reported_collisions), + }, + }), + sable_data, + ground_handle: None, + reported_collisions, + current_step_vm, + gravity: Vec3::new(x as Real, y as Real, z as Real), + universal_drag: universal_drag as Real, + manifold_info_map, + }; - normalized_max_corrective_velocity: 50.0, - normalized_allowed_linear_error: 0.0025, - - ..IntegrationParameters::default() - }, - voxel_collider_map: VoxelColliderMap::new(), - }) - }); - - let ground = RigidBodyBuilder::fixed(); + { + let mut sim_data = scene.sim_data.write().unwrap(); + sim_data.collider_set.insert(collider); - let collider = ColliderBuilder::new(SharedShape::new(LevelCollider::new(None, true))) - .collision_groups(LEVEL_GROUP) - .build(); + scene.ground_handle = Some(sim_data.rigid_body_set.insert(ground)); + } - let sable_data = Arc::new(RwLock::new(SableSceneData { - main_level_chunks: HashMap::::new(), - octree_chunks: HashMap::::new(), - joint_set: SableJointSet::new(), - rope_map: RopeMap::default(), - level_colliders: HashMap::::new(), - rigid_bodies: HashMap::::new(), - })); - let manifold_info_map = Arc::new(SableManifoldInfoMap::default()); - let reported_collisions = Arc::new(ReportedCollisionBuffer::new()); - let current_step_vm = Some(Arc::new(unsafe { - JavaVM::from_raw(env.get_java_vm().unwrap().get_java_vm_pointer()).unwrap() + info!("Rapier scene initialized"); + Arc::into_raw(Arc::new(scene)) as jlong })); - let dispatcher = SableDispatcher { - sable_data: Arc::clone(&sable_data), - manifold_info_map: Arc::clone(&manifold_info_map), - }; - - let mut scene = PhysicsScene { - sim_data: RwLock::new(SimulationSceneData { - pipeline: PhysicsPipeline::new(), - rigid_body_set: RigidBodySet::new(), - collider_set: ColliderSet::new(), - island_manager: IslandManager::new(), - broad_phase: DefaultBroadPhase::new(), - narrow_phase: NarrowPhase::with_query_dispatcher( - dispatcher.chain(DefaultQueryDispatcher), - ), - impulse_joint_set: ImpulseJointSet::new(), - multibody_joint_set: MultibodyJointSet::new(), - ccd_solver: CCDSolver::new(), - physics_hooks: SablePhysicsHooks { - sable_data: Arc::clone(&sable_data), - manifold_info_map: Arc::clone(&manifold_info_map), - current_step_vm: current_step_vm.clone(), - }, - event_handler: SableEventHandler { - reported_collisions: Arc::clone(&reported_collisions), - }, - }), - sable_data, - ground_handle: None, - reported_collisions, - current_step_vm, - gravity: Vec3::new(x as Real, y as Real, z as Real), - universal_drag: universal_drag as Real, - manifold_info_map, - }; - - { - let mut sim_data = scene.sim_data.write().unwrap(); - sim_data.collider_set.insert(collider); - - scene.ground_handle = Some(sim_data.rigid_body_set.insert(ground)); + match result { + Ok(handle) => handle, + Err(payload) => { + let msg = format!( + "Rapier native panic during initialize: {}", + panic_message(&payload) + ); + let _ = env.throw_new("java/lang/RuntimeException", &msg); + 0 + } } - - info!("Rapier scene initialized"); - Arc::into_raw(Arc::new(scene)) as jlong } #[unsafe(no_mangle)]