1use crate::{
4 calculate_zero_angle_with_conditions, run_monte_carlo_with_direction_std_dev,
5 AtmosphericConditions, BallisticInputs, DragModel, MonteCarloParams, TrajectorySolver,
6 WindConditions,
7};
8use std::os::raw::{c_char, c_double, c_int};
9use std::ptr;
10
11pub const MIN_FFI_STEP_SIZE_MS: c_double = 0.1;
16
17pub const MAX_FFI_DRAG_TABLE_LEN: c_int = 4096;
25
26#[repr(C)]
29pub struct FFIBallisticInputs {
30 pub muzzle_velocity: c_double, pub muzzle_angle: c_double, pub bc_value: c_double, pub bullet_mass: c_double, pub bullet_diameter: c_double, pub bc_type: c_int, pub sight_height: c_double, pub target_distance: c_double, pub temperature: c_double, pub twist_rate: c_double, pub is_twist_right: c_int, pub shooting_angle: c_double, pub altitude: c_double, pub latitude: c_double, pub azimuth_angle: c_double, pub use_rk4: c_int, pub use_adaptive_rk45: c_int, pub enable_wind_shear: c_int, pub enable_trajectory_sampling: c_int, pub sample_interval: c_double, pub enable_pitch_damping: c_int, pub enable_precession_nutation: c_int, pub enable_spin_drift: c_int, pub enable_magnus: c_int, pub enable_coriolis: c_int, pub shot_azimuth: c_double,
59 pub cant_angle: c_double,
63}
64
65#[repr(C)]
66pub struct FFIWindConditions {
67 pub speed: c_double, pub direction: c_double,
71 pub vertical_speed: c_double,
74}
75
76#[repr(C)]
77pub struct FFIAtmosphericConditions {
78 pub temperature: c_double, pub pressure: c_double, pub humidity: c_double, pub altitude: c_double, }
83
84#[repr(C)]
85pub struct FFITrajectorySample {
86 pub distance: c_double, pub time: c_double, pub velocity_mps: c_double, pub energy_joules: c_double, pub drop_meters: c_double, pub windage_meters: c_double, pub mach: c_double, pub spin_rate_rps: c_double, }
95
96#[repr(C)]
97pub struct FFITrajectoryPoint {
98 pub time: c_double,
99 pub position_x: c_double,
100 pub position_y: c_double,
101 pub position_z: c_double,
102 pub velocity_magnitude: c_double,
103 pub kinetic_energy: c_double,
104}
105
106#[repr(C)]
107pub struct FFITrajectoryResult {
108 pub max_range: c_double,
109 pub max_height: c_double,
110 pub time_of_flight: c_double,
111 pub impact_velocity: c_double,
112 pub impact_energy: c_double,
113 pub points: *mut FFITrajectoryPoint,
114 pub point_count: c_int,
115 pub sampled_points: *mut FFITrajectorySample,
116 pub sampled_point_count: c_int,
117 pub min_pitch_damping: c_double, pub transonic_mach: c_double, pub final_pitch_angle: c_double, pub final_yaw_angle: c_double, pub max_yaw_angle: c_double, pub max_precession_angle: c_double, }
124
125#[repr(C)]
127pub struct FFIMonteCarloParams {
128 pub num_simulations: c_int,
129 pub velocity_std_dev: c_double,
130 pub angle_std_dev: c_double,
131 pub bc_std_dev: c_double,
132 pub wind_speed_std_dev: c_double,
133 pub target_distance: c_double, pub base_wind_speed: c_double, pub base_wind_direction: c_double, pub azimuth_std_dev: c_double, }
138
139#[repr(C)]
141pub struct FFIMonteCarloResults {
142 pub ranges: *mut c_double,
143 pub impact_velocities: *mut c_double,
144 pub impact_positions_x: *mut c_double,
145 pub impact_positions_y: *mut c_double,
148 pub impact_positions_z: *mut c_double,
149 pub num_results: c_int,
150 pub mean_range: c_double,
151 pub std_dev_range: c_double,
152 pub mean_impact_velocity: c_double,
153 pub std_dev_impact_velocity: c_double,
154 pub hit_probability: c_double, }
156
157#[allow(clippy::field_reassign_with_default)] fn convert_inputs(inputs: &FFIBallisticInputs) -> BallisticInputs {
160 let mut ballistic_inputs = BallisticInputs::default();
161
162 ballistic_inputs.muzzle_velocity = inputs.muzzle_velocity;
163 ballistic_inputs.muzzle_angle = inputs.muzzle_angle;
164 ballistic_inputs.azimuth_angle = inputs.azimuth_angle;
165 ballistic_inputs.shot_azimuth = inputs.shot_azimuth;
166 ballistic_inputs.cant_angle = inputs.cant_angle;
167 ballistic_inputs.use_rk4 = inputs.use_rk4 != 0;
168 ballistic_inputs.use_adaptive_rk45 = inputs.use_adaptive_rk45 != 0;
169 ballistic_inputs.bc_value = inputs.bc_value;
170 ballistic_inputs.bullet_mass = inputs.bullet_mass;
171 ballistic_inputs.bullet_diameter = inputs.bullet_diameter;
172 ballistic_inputs.bc_type = match inputs.bc_type {
173 1 => DragModel::G7,
174 2 => DragModel::G2,
175 3 => DragModel::G5,
176 4 => DragModel::G6,
177 5 => DragModel::G8,
178 6 => DragModel::GI,
179 7 => DragModel::GS,
180 _ => DragModel::G1,
181 };
182 ballistic_inputs.sight_height = inputs.sight_height;
183 ballistic_inputs.target_distance = inputs.target_distance;
184 ballistic_inputs.temperature = inputs.temperature;
185 ballistic_inputs.twist_rate = inputs.twist_rate;
186 ballistic_inputs.is_twist_right = inputs.is_twist_right != 0;
187 ballistic_inputs.shooting_angle = inputs.shooting_angle;
188 ballistic_inputs.altitude = inputs.altitude;
189
190 if !inputs.latitude.is_nan() {
191 ballistic_inputs.latitude = Some(inputs.latitude);
192 }
193
194 ballistic_inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
196 ballistic_inputs.weight_grains = inputs.bullet_mass / crate::constants::GRAINS_TO_KG;
197 ballistic_inputs.bullet_length = {
200 let est = crate::stability::estimate_bullet_length_m(
201 ballistic_inputs.bullet_diameter,
202 ballistic_inputs.bullet_mass,
203 );
204 if est > 0.0 {
205 est
206 } else {
207 ballistic_inputs.bullet_diameter * 4.5
208 }
209 };
210
211 ballistic_inputs.enable_wind_shear = inputs.enable_wind_shear != 0;
213 ballistic_inputs.enable_trajectory_sampling = inputs.enable_trajectory_sampling != 0;
214 ballistic_inputs.sample_interval = inputs.sample_interval;
215 ballistic_inputs.enable_pitch_damping = inputs.enable_pitch_damping != 0;
216 ballistic_inputs.enable_precession_nutation = inputs.enable_precession_nutation != 0;
217 ballistic_inputs.use_enhanced_spin_drift = inputs.enable_spin_drift != 0;
218 ballistic_inputs.enable_advanced_effects =
219 inputs.enable_magnus != 0 || inputs.enable_coriolis != 0;
220 ballistic_inputs.enable_magnus = inputs.enable_magnus != 0;
222 ballistic_inputs.enable_coriolis = inputs.enable_coriolis != 0;
223
224 ballistic_inputs
225}
226
227unsafe fn drag_table_from_raw(
241 mach: *const c_double,
242 cd: *const c_double,
243 len: c_int,
244) -> Result<crate::drag::DragTable, ()> {
245 if mach.is_null() || cd.is_null() || !(2..=MAX_FFI_DRAG_TABLE_LEN).contains(&len) {
246 return Err(());
247 }
248 let len = len as usize;
249 let mach = unsafe { std::slice::from_raw_parts(mach, len) }.to_vec();
250 let cd = unsafe { std::slice::from_raw_parts(cd, len) }.to_vec();
251 crate::drag::DragTable::try_new(mach, cd).map_err(|_| ())
252}
253
254unsafe fn calculate_trajectory_impl(
258 inputs: *const FFIBallisticInputs,
259 wind: *const FFIWindConditions,
260 atmosphere: *const FFIAtmosphericConditions,
261 max_range: c_double,
262 step_size: c_double,
263 custom_drag_table: Option<crate::drag::DragTable>,
264) -> *mut FFITrajectoryResult {
265 if inputs.is_null() {
266 return ptr::null_mut();
267 }
268 if !step_size.is_finite() || step_size < MIN_FFI_STEP_SIZE_MS {
269 return ptr::null_mut();
270 }
271
272 let inputs = unsafe { &*inputs };
273 let mut ballistic_inputs = convert_inputs(inputs);
274 ballistic_inputs.custom_drag_table = custom_drag_table;
275 let twist_rate_in = ballistic_inputs.twist_rate;
276
277 let wind_conditions = if wind.is_null() {
278 WindConditions::default()
279 } else {
280 let wind = unsafe { &*wind };
281 WindConditions {
282 speed: wind.speed,
283 direction: wind.direction,
284 vertical_speed: wind.vertical_speed,
285 }
286 };
287
288 let atmospheric_conditions = if atmosphere.is_null() {
289 AtmosphericConditions::default()
290 } else {
291 let atmo = unsafe { &*atmosphere };
292 AtmosphericConditions {
293 temperature: atmo.temperature,
294 pressure: atmo.pressure,
295 humidity: atmo.humidity,
296 altitude: atmo.altitude,
297 }
298 };
299
300 let (sample_temp_c, sample_pressure_hpa) = crate::atmosphere::resolve_station_conditions(
302 atmospheric_conditions.temperature,
303 atmospheric_conditions.pressure,
304 atmospheric_conditions.altitude,
305 );
306 let (_, sample_speed_of_sound) = crate::atmosphere::calculate_atmosphere(
307 atmospheric_conditions.altitude,
308 Some(sample_temp_c),
309 Some(sample_pressure_hpa),
310 atmospheric_conditions.humidity,
311 );
312
313 let mut solver =
314 TrajectorySolver::new(ballistic_inputs, wind_conditions, atmospheric_conditions);
315
316 solver.set_max_range(max_range);
318 solver.set_time_step(step_size / 1000.0); match solver.solve() {
321 Ok(result) => {
322 let point_count = result.points.len();
324 let points = if point_count > 0 {
325 let mut ffi_points = Vec::with_capacity(point_count);
326 for point in result.points.iter() {
327 ffi_points.push(FFITrajectoryPoint {
328 time: point.time,
329 position_x: point.position[0],
330 position_y: point.position[1],
331 position_z: point.position[2],
332 velocity_magnitude: point.velocity_magnitude,
333 kinetic_energy: point.kinetic_energy,
334 });
335 }
336 let points_ptr = ffi_points.as_mut_ptr();
337 std::mem::forget(ffi_points); points_ptr
339 } else {
340 ptr::null_mut()
341 };
342
343 let (sampled_points, sampled_point_count) =
345 if let Some(ref samples) = result.sampled_points {
346 let mut ffi_samples = Vec::with_capacity(samples.len());
347 for sample in samples {
348 ffi_samples.push(FFITrajectorySample {
349 distance: sample.distance_m,
350 time: sample.time_s,
351 velocity_mps: sample.velocity_mps,
352 energy_joules: sample.energy_j,
353 drop_meters: sample.drop_m,
354 windage_meters: sample.wind_drift_m,
355 mach: if sample_speed_of_sound > 0.0 {
356 sample.velocity_mps / sample_speed_of_sound
357 } else {
358 0.0
359 },
360 spin_rate_rps: if twist_rate_in > 0.0 {
361 sample.velocity_mps / (twist_rate_in * 0.0254)
362 } else {
363 0.0
364 },
365 });
366 }
367 let count = ffi_samples.len() as c_int;
368 let samples_ptr = ffi_samples.as_mut_ptr();
369 std::mem::forget(ffi_samples);
370 (samples_ptr, count)
371 } else {
372 (ptr::null_mut(), 0)
373 };
374
375 let (final_pitch, final_yaw, max_yaw, max_prec) =
377 if let Some(ref angular) = result.angular_state {
378 (
379 angular.pitch_angle,
380 angular.yaw_angle,
381 result.max_yaw_angle.unwrap_or(f64::NAN),
382 result.max_precession_angle.unwrap_or(f64::NAN),
383 )
384 } else {
385 (f64::NAN, f64::NAN, f64::NAN, f64::NAN)
386 };
387
388 let ffi_result = Box::new(FFITrajectoryResult {
390 max_range: result.max_range,
391 max_height: result.max_height,
392 time_of_flight: result.time_of_flight,
393 impact_velocity: result.impact_velocity,
394 impact_energy: result.impact_energy,
395 points,
396 point_count: point_count as c_int,
397 sampled_points,
398 sampled_point_count,
399 min_pitch_damping: result.min_pitch_damping.unwrap_or(f64::NAN),
400 transonic_mach: result.transonic_mach.unwrap_or(f64::NAN),
401 final_pitch_angle: final_pitch,
402 final_yaw_angle: final_yaw,
403 max_yaw_angle: max_yaw,
404 max_precession_angle: max_prec,
405 });
406
407 Box::into_raw(ffi_result)
408 }
409 Err(_) => ptr::null_mut(),
410 }
411}
412
413#[no_mangle]
432pub unsafe extern "C" fn ballistics_calculate_trajectory(
433 inputs: *const FFIBallisticInputs,
434 wind: *const FFIWindConditions,
435 atmosphere: *const FFIAtmosphericConditions,
436 max_range: c_double,
437 step_size: c_double,
438) -> *mut FFITrajectoryResult {
439 unsafe { calculate_trajectory_impl(inputs, wind, atmosphere, max_range, step_size, None) }
440}
441
442#[no_mangle]
463pub unsafe extern "C" fn ballistics_calculate_trajectory_with_drag_table(
464 inputs: *const FFIBallisticInputs,
465 wind: *const FFIWindConditions,
466 atmosphere: *const FFIAtmosphericConditions,
467 max_range: c_double,
468 step_size: c_double,
469 drag_mach: *const c_double,
470 drag_cd: *const c_double,
471 drag_table_len: c_int,
472) -> *mut FFITrajectoryResult {
473 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
474 Ok(t) => t,
475 Err(()) => return ptr::null_mut(),
476 };
477 unsafe {
478 calculate_trajectory_impl(inputs, wind, atmosphere, max_range, step_size, Some(table))
479 }
480}
481
482#[no_mangle]
491pub unsafe extern "C" fn ballistics_free_trajectory_result(result: *mut FFITrajectoryResult) {
492 if !result.is_null() {
493 unsafe {
494 let result = Box::from_raw(result);
495 if !result.points.is_null() && result.point_count > 0 {
496 let points = Vec::from_raw_parts(
497 result.points,
498 result.point_count as usize,
499 result.point_count as usize,
500 );
501 drop(points);
502 }
503 if !result.sampled_points.is_null() && result.sampled_point_count > 0 {
504 let samples = Vec::from_raw_parts(
505 result.sampled_points,
506 result.sampled_point_count as usize,
507 result.sampled_point_count as usize,
508 );
509 drop(samples);
510 }
511 drop(result);
512 }
513 }
514}
515
516unsafe fn calculate_zero_angle_impl(
522 inputs: *const FFIBallisticInputs,
523 wind: *const FFIWindConditions,
524 atmosphere: *const FFIAtmosphericConditions,
525 zero_distance: c_double,
526 custom_drag_table: Option<crate::drag::DragTable>,
527) -> c_double {
528 if inputs.is_null() {
529 return f64::NAN;
530 }
531
532 let inputs = unsafe { &*inputs };
533 let mut ballistic_inputs = convert_inputs(inputs);
534 ballistic_inputs.custom_drag_table = custom_drag_table;
535
536 let wind_conditions = if wind.is_null() {
537 WindConditions::default()
538 } else {
539 let wind = unsafe { &*wind };
540 WindConditions {
541 speed: wind.speed,
542 direction: wind.direction,
543 vertical_speed: wind.vertical_speed,
544 }
545 };
546
547 let atmospheric_conditions = if atmosphere.is_null() {
548 AtmosphericConditions::default()
549 } else {
550 let atmo = unsafe { &*atmosphere };
551 AtmosphericConditions {
552 temperature: atmo.temperature,
553 pressure: atmo.pressure,
554 humidity: atmo.humidity,
555 altitude: atmo.altitude,
556 }
557 };
558
559 let target_height = ballistic_inputs.sight_height;
562
563 calculate_zero_angle_with_conditions(
564 ballistic_inputs,
565 zero_distance,
566 target_height,
567 wind_conditions,
568 atmospheric_conditions,
569 )
570 .unwrap_or(f64::NAN)
571}
572
573#[no_mangle]
583pub unsafe extern "C" fn ballistics_calculate_zero_angle(
584 inputs: *const FFIBallisticInputs,
585 wind: *const FFIWindConditions,
586 atmosphere: *const FFIAtmosphericConditions,
587 zero_distance: c_double,
588) -> c_double {
589 unsafe { calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, None) }
590}
591
592#[no_mangle]
614pub unsafe extern "C" fn ballistics_calculate_zero_angle_with_drag_table(
615 inputs: *const FFIBallisticInputs,
616 wind: *const FFIWindConditions,
617 atmosphere: *const FFIAtmosphericConditions,
618 zero_distance: c_double,
619 drag_mach: *const c_double,
620 drag_cd: *const c_double,
621 drag_table_len: c_int,
622) -> c_double {
623 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
624 Ok(t) => t,
625 Err(()) => return f64::NAN,
626 };
627 unsafe { calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, Some(table)) }
628}
629
630#[no_mangle]
632#[allow(clippy::field_reassign_with_default)] pub extern "C" fn ballistics_quick_trajectory(
634 muzzle_velocity: c_double,
635 bc: c_double,
636 sight_height: c_double,
637 zero_distance: c_double,
638 target_distance: c_double,
639) -> c_double {
640 let mut inputs = BallisticInputs::default();
644 inputs.muzzle_velocity = muzzle_velocity;
645 inputs.bc_value = bc;
646 inputs.sight_height = sight_height;
647 inputs.target_distance = target_distance;
648
649 let wind = WindConditions::default();
650 let atmo = AtmosphericConditions::default();
651
652 let zero_angle = match calculate_zero_angle_with_conditions(
654 inputs.clone(),
655 zero_distance,
656 sight_height,
657 wind.clone(),
658 atmo.clone(),
659 ) {
660 Ok(angle) => angle,
661 Err(_) => return f64::NAN,
662 };
663
664 inputs.muzzle_angle = zero_angle;
666
667 let mut solver = TrajectorySolver::new(inputs, wind, atmo);
668 solver.set_max_range(target_distance * 1.1);
669
670 match solver.solve() {
671 Ok(result) => {
672 for point in result.points {
674 if point.position[0] >= target_distance {
675 return sight_height - point.position[1];
676 }
677 }
678 f64::NAN
679 }
680 Err(_) => f64::NAN,
681 }
682}
683
684#[no_mangle]
696pub unsafe extern "C" fn ballistics_monte_carlo(
697 inputs: *const FFIBallisticInputs,
698 atmosphere: *const FFIAtmosphericConditions,
699 params: *const FFIMonteCarloParams,
700) -> *mut FFIMonteCarloResults {
701 unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, 0.0) }
702}
703
704#[no_mangle]
714pub unsafe extern "C" fn ballistics_monte_carlo_with_direction_std_dev(
715 inputs: *const FFIBallisticInputs,
716 atmosphere: *const FFIAtmosphericConditions,
717 params: *const FFIMonteCarloParams,
718 wind_direction_std_dev: c_double,
719) -> *mut FFIMonteCarloResults {
720 unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, wind_direction_std_dev) }
721}
722
723unsafe fn ballistics_monte_carlo_impl(
724 inputs: *const FFIBallisticInputs,
725 atmosphere: *const FFIAtmosphericConditions,
726 params: *const FFIMonteCarloParams,
727 wind_direction_std_dev: f64,
728) -> *mut FFIMonteCarloResults {
729 if inputs.is_null() || params.is_null() {
730 return ptr::null_mut();
731 }
732
733 let inputs = unsafe { &*inputs };
734 let params = unsafe { &*params };
735
736 const MAX_SIMULATIONS: c_int = 1_000_000;
742 if params.num_simulations <= 0 || params.num_simulations > MAX_SIMULATIONS {
743 return ptr::null_mut();
744 }
745
746 let mut ballistic_inputs = convert_inputs(inputs);
748 ballistic_inputs.muzzle_height = 1.5;
749 ballistic_inputs.ground_threshold = 0.0;
750 if !atmosphere.is_null() {
751 let atmo = unsafe { &*atmosphere };
752 ballistic_inputs.temperature = atmo.temperature;
753 ballistic_inputs.pressure = atmo.pressure;
754 ballistic_inputs.humidity = (atmo.humidity / 100.0).clamp(0.0, 1.0);
755 ballistic_inputs.altitude = atmo.altitude;
756 }
757
758 let mc_params = MonteCarloParams {
760 num_simulations: params.num_simulations as usize,
761 velocity_std_dev: params.velocity_std_dev,
762 angle_std_dev: params.angle_std_dev,
763 bc_std_dev: params.bc_std_dev,
764 wind_speed_std_dev: params.wind_speed_std_dev,
765 target_distance: if params.target_distance.is_nan() {
766 None
767 } else {
768 Some(params.target_distance)
769 },
770 base_wind_speed: params.base_wind_speed,
771 base_wind_direction: params.base_wind_direction,
772 azimuth_std_dev: params.azimuth_std_dev,
773 };
774
775 match run_monte_carlo_with_direction_std_dev(
777 ballistic_inputs,
778 mc_params,
779 wind_direction_std_dev,
780 ) {
781 Ok(results) => {
782 let num_results = results.ranges.len() as c_int;
783
784 let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
786 let variance_range: f64 = results
787 .ranges
788 .iter()
789 .map(|r| (r - mean_range).powi(2))
790 .sum::<f64>()
791 / num_results as f64;
792 let std_dev_range = variance_range.sqrt();
793
794 let mean_velocity: f64 =
795 results.impact_velocities.iter().sum::<f64>() / num_results as f64;
796 let variance_velocity: f64 = results
797 .impact_velocities
798 .iter()
799 .map(|v| (v - mean_velocity).powi(2))
800 .sum::<f64>()
801 / num_results as f64;
802 let std_dev_velocity = variance_velocity.sqrt();
803
804 let hit_probability = if params.target_distance.is_nan() {
810 0.0
811 } else {
812 results.hit_probability(crate::DEFAULT_HIT_RADIUS_M)
813 };
814
815 let ranges_ptr = unsafe {
817 let ptr = std::alloc::alloc(
818 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
819 ) as *mut c_double;
820 for (i, &range) in results.ranges.iter().enumerate() {
821 *ptr.add(i) = range;
822 }
823 ptr
824 };
825
826 let velocities_ptr = unsafe {
827 let ptr = std::alloc::alloc(
828 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
829 ) as *mut c_double;
830 for (i, &vel) in results.impact_velocities.iter().enumerate() {
831 *ptr.add(i) = vel;
832 }
833 ptr
834 };
835
836 let pos_x_ptr = unsafe {
837 let ptr = std::alloc::alloc(
838 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
839 ) as *mut c_double;
840 for (i, pos) in results.impact_positions.iter().enumerate() {
841 *ptr.add(i) = pos.x;
842 }
843 ptr
844 };
845
846 let pos_y_ptr = unsafe {
847 let ptr = std::alloc::alloc(
848 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
849 ) as *mut c_double;
850 for (i, pos) in results.impact_positions.iter().enumerate() {
851 *ptr.add(i) = pos.y;
852 }
853 ptr
854 };
855
856 let pos_z_ptr = unsafe {
857 let ptr = std::alloc::alloc(
858 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
859 ) as *mut c_double;
860 for (i, pos) in results.impact_positions.iter().enumerate() {
861 *ptr.add(i) = pos.z;
862 }
863 ptr
864 };
865
866 let result = Box::new(FFIMonteCarloResults {
868 ranges: ranges_ptr,
869 impact_velocities: velocities_ptr,
870 impact_positions_x: pos_x_ptr,
871 impact_positions_y: pos_y_ptr,
872 impact_positions_z: pos_z_ptr,
873 num_results,
874 mean_range,
875 std_dev_range,
876 mean_impact_velocity: mean_velocity,
877 std_dev_impact_velocity: std_dev_velocity,
878 hit_probability,
879 });
880
881 Box::into_raw(result)
882 }
883 Err(_) => ptr::null_mut(),
884 }
885}
886
887#[no_mangle]
896pub unsafe extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
897 if results.is_null() {
898 return;
899 }
900
901 unsafe {
902 let results = Box::from_raw(results);
903 let num = results.num_results as usize;
904
905 if !results.ranges.is_null() {
907 std::alloc::dealloc(
908 results.ranges as *mut u8,
909 std::alloc::Layout::array::<c_double>(num).unwrap(),
910 );
911 }
912
913 if !results.impact_velocities.is_null() {
914 std::alloc::dealloc(
915 results.impact_velocities as *mut u8,
916 std::alloc::Layout::array::<c_double>(num).unwrap(),
917 );
918 }
919
920 if !results.impact_positions_x.is_null() {
921 std::alloc::dealloc(
922 results.impact_positions_x as *mut u8,
923 std::alloc::Layout::array::<c_double>(num).unwrap(),
924 );
925 }
926
927 if !results.impact_positions_y.is_null() {
928 std::alloc::dealloc(
929 results.impact_positions_y as *mut u8,
930 std::alloc::Layout::array::<c_double>(num).unwrap(),
931 );
932 }
933
934 if !results.impact_positions_z.is_null() {
935 std::alloc::dealloc(
936 results.impact_positions_z as *mut u8,
937 std::alloc::Layout::array::<c_double>(num).unwrap(),
938 );
939 }
940
941 }
943}
944
945#[no_mangle]
947pub extern "C" fn ballistics_get_version() -> *const c_char {
948 concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
952}
953
954#[cfg(test)]
955mod tests {
956 use super::*;
957
958 fn valid_trajectory_inputs() -> FFIBallisticInputs {
959 FFIBallisticInputs {
960 muzzle_velocity: 800.0,
961 muzzle_angle: 0.0,
962 bc_value: 0.5,
963 bullet_mass: 0.01,
964 bullet_diameter: 0.00762,
965 bc_type: 0,
966 sight_height: 0.05,
967 target_distance: 1.0,
968 temperature: 15.0,
969 twist_rate: 12.0,
970 is_twist_right: 1,
971 shooting_angle: 0.0,
972 altitude: 0.0,
973 latitude: f64::NAN,
974 azimuth_angle: 0.0,
975 use_rk4: 1,
976 use_adaptive_rk45: 0,
977 enable_wind_shear: 0,
978 enable_trajectory_sampling: 0,
979 sample_interval: 10.0,
980 enable_pitch_damping: 0,
981 enable_precession_nutation: 0,
982 enable_spin_drift: 0,
983 enable_magnus: 0,
984 enable_coriolis: 0,
985 shot_azimuth: 0.0,
986 cant_angle: 0.0,
987 }
988 }
989
990 #[allow(dead_code)]
991 #[repr(C)]
992 struct LegacyFFIMonteCarloParams {
993 num_simulations: c_int,
994 velocity_std_dev: c_double,
995 angle_std_dev: c_double,
996 bc_std_dev: c_double,
997 wind_speed_std_dev: c_double,
998 target_distance: c_double,
999 base_wind_speed: c_double,
1000 base_wind_direction: c_double,
1001 azimuth_std_dev: c_double,
1002 }
1003
1004 #[test]
1005 fn monte_carlo_params_legacy_abi_size_is_unchanged() {
1006 assert_eq!(
1007 std::mem::size_of::<FFIMonteCarloParams>(),
1008 std::mem::size_of::<LegacyFFIMonteCarloParams>()
1009 );
1010 assert_eq!(
1011 std::mem::align_of::<FFIMonteCarloParams>(),
1012 std::mem::align_of::<LegacyFFIMonteCarloParams>()
1013 );
1014 }
1015
1016 #[test]
1017 fn null_pointer_contracts_return_sentinels_and_free_safely() {
1018 unsafe {
1019 assert!(ballistics_calculate_trajectory(
1020 std::ptr::null(),
1021 std::ptr::null(),
1022 std::ptr::null(),
1023 1_000.0,
1024 1.0,
1025 )
1026 .is_null());
1027 assert!(ballistics_calculate_zero_angle(
1028 std::ptr::null(),
1029 std::ptr::null(),
1030 std::ptr::null(),
1031 100.0,
1032 )
1033 .is_nan());
1034 assert!(ballistics_calculate_trajectory_with_drag_table(
1035 std::ptr::null(),
1036 std::ptr::null(),
1037 std::ptr::null(),
1038 1_000.0,
1039 1.0,
1040 DECK_MACH.as_ptr(),
1041 DECK_CD_LOW.as_ptr(),
1042 DECK_MACH.len() as c_int,
1043 )
1044 .is_null());
1045 assert!(ballistics_calculate_zero_angle_with_drag_table(
1046 std::ptr::null(),
1047 std::ptr::null(),
1048 std::ptr::null(),
1049 100.0,
1050 DECK_MACH.as_ptr(),
1051 DECK_CD_LOW.as_ptr(),
1052 DECK_MACH.len() as c_int,
1053 )
1054 .is_nan());
1055 assert!(
1056 ballistics_monte_carlo(std::ptr::null(), std::ptr::null(), std::ptr::null(),)
1057 .is_null()
1058 );
1059 assert!(ballistics_monte_carlo_with_direction_std_dev(
1060 std::ptr::null(),
1061 std::ptr::null(),
1062 std::ptr::null(),
1063 0.1,
1064 )
1065 .is_null());
1066
1067 ballistics_free_trajectory_result(std::ptr::null_mut());
1068 ballistics_free_monte_carlo_results(std::ptr::null_mut());
1069 }
1070 }
1071
1072 #[test]
1073 fn mba1283_ffi_enforces_step_floor_for_every_solver_mode() {
1074 for (mode, use_rk4, use_adaptive_rk45) in [("Euler", 0, 0), ("RK4", 1, 0), ("RK45", 1, 1)] {
1075 for step_size in [
1076 f64::NAN,
1077 f64::INFINITY,
1078 f64::NEG_INFINITY,
1079 -1.0,
1080 -0.0,
1081 0.0,
1082 0.001,
1083 MIN_FFI_STEP_SIZE_MS - 0.001,
1084 ] {
1085 let mut inputs = valid_trajectory_inputs();
1086 inputs.use_rk4 = use_rk4;
1087 inputs.use_adaptive_rk45 = use_adaptive_rk45;
1088 let result = unsafe {
1089 ballistics_calculate_trajectory(
1090 &inputs,
1091 std::ptr::null(),
1092 std::ptr::null(),
1093 0.01,
1094 step_size,
1095 )
1096 };
1097 assert!(
1098 result.is_null(),
1099 "{mode} step_size={step_size:?} bypassed the FFI floor"
1100 );
1101 }
1102
1103 let mut inputs = valid_trajectory_inputs();
1104 inputs.use_rk4 = use_rk4;
1105 inputs.use_adaptive_rk45 = use_adaptive_rk45;
1106 let result = unsafe {
1107 ballistics_calculate_trajectory(
1108 &inputs,
1109 std::ptr::null(),
1110 std::ptr::null(),
1111 0.01,
1112 MIN_FFI_STEP_SIZE_MS,
1113 )
1114 };
1115 assert!(
1116 !result.is_null(),
1117 "the documented minimum step must remain usable in {mode}"
1118 );
1119 unsafe {
1120 assert!((*result).point_count >= 0);
1121 assert!((*result).point_count as usize <= crate::MAX_TRAJECTORY_POINTS);
1122 ballistics_free_trajectory_result(result);
1123 }
1124 }
1125 }
1126
1127 const DECK_MACH: [f64; 4] = [0.5, 1.0, 2.0, 3.0];
1129 const DECK_CD_LOW: [f64; 4] = [0.05, 0.08, 0.06, 0.05];
1131
1132 #[test]
1133 fn trajectory_with_drag_table_applies_the_deck() {
1134 let inputs = valid_trajectory_inputs();
1135 unsafe {
1136 let plain = ballistics_calculate_trajectory(
1137 &inputs,
1138 std::ptr::null(),
1139 std::ptr::null(),
1140 300.0,
1141 1.0,
1142 );
1143 let decked = ballistics_calculate_trajectory_with_drag_table(
1144 &inputs,
1145 std::ptr::null(),
1146 std::ptr::null(),
1147 300.0,
1148 1.0,
1149 DECK_MACH.as_ptr(),
1150 DECK_CD_LOW.as_ptr(),
1151 DECK_MACH.len() as c_int,
1152 );
1153 assert!(!plain.is_null() && !decked.is_null());
1154 assert!(
1156 (*decked).impact_velocity > (*plain).impact_velocity + 1.0,
1157 "deck did not change the solve: plain={} decked={}",
1158 (*plain).impact_velocity,
1159 (*decked).impact_velocity
1160 );
1161 ballistics_free_trajectory_result(plain);
1162 ballistics_free_trajectory_result(decked);
1163 }
1164 }
1165
1166 #[test]
1167 fn trajectory_with_drag_table_rejects_invalid_decks() {
1168 let inputs = valid_trajectory_inputs();
1169 let descending = [3.0, 2.0, 1.0, 0.5];
1170 let negative_cd = [0.05, -0.08, 0.06, 0.05];
1171 unsafe {
1172 assert!(ballistics_calculate_trajectory_with_drag_table(
1174 &inputs,
1175 std::ptr::null(),
1176 std::ptr::null(),
1177 300.0,
1178 1.0,
1179 std::ptr::null(),
1180 DECK_CD_LOW.as_ptr(),
1181 4,
1182 )
1183 .is_null());
1184 assert!(ballistics_calculate_trajectory_with_drag_table(
1185 &inputs,
1186 std::ptr::null(),
1187 std::ptr::null(),
1188 300.0,
1189 1.0,
1190 DECK_MACH.as_ptr(),
1191 std::ptr::null(),
1192 4,
1193 )
1194 .is_null());
1195 assert!(ballistics_calculate_trajectory_with_drag_table(
1197 &inputs,
1198 std::ptr::null(),
1199 std::ptr::null(),
1200 300.0,
1201 1.0,
1202 DECK_MACH.as_ptr(),
1203 DECK_CD_LOW.as_ptr(),
1204 1,
1205 )
1206 .is_null());
1207 assert!(ballistics_calculate_trajectory_with_drag_table(
1209 &inputs,
1210 std::ptr::null(),
1211 std::ptr::null(),
1212 300.0,
1213 1.0,
1214 descending.as_ptr(),
1215 DECK_CD_LOW.as_ptr(),
1216 4,
1217 )
1218 .is_null());
1219 assert!(ballistics_calculate_trajectory_with_drag_table(
1221 &inputs,
1222 std::ptr::null(),
1223 std::ptr::null(),
1224 300.0,
1225 1.0,
1226 DECK_MACH.as_ptr(),
1227 negative_cd.as_ptr(),
1228 4,
1229 )
1230 .is_null());
1231 assert!(ballistics_calculate_trajectory_with_drag_table(
1233 std::ptr::null(),
1234 std::ptr::null(),
1235 std::ptr::null(),
1236 300.0,
1237 1.0,
1238 DECK_MACH.as_ptr(),
1239 DECK_CD_LOW.as_ptr(),
1240 4,
1241 )
1242 .is_null());
1243 }
1244 }
1245
1246 #[test]
1247 fn zero_angle_with_drag_table_applies_the_deck() {
1248 let inputs = valid_trajectory_inputs();
1250 unsafe {
1251 let plain =
1252 ballistics_calculate_zero_angle(&inputs, std::ptr::null(), std::ptr::null(), 100.0);
1253 let decked = ballistics_calculate_zero_angle_with_drag_table(
1254 &inputs,
1255 std::ptr::null(),
1256 std::ptr::null(),
1257 100.0,
1258 DECK_MACH.as_ptr(),
1259 DECK_CD_LOW.as_ptr(),
1260 DECK_MACH.len() as c_int,
1261 );
1262 assert!(plain.is_finite() && decked.is_finite());
1263 assert!(
1266 (plain - decked).abs() > 1e-6,
1267 "deck did not change the zero: plain={plain} decked={decked}"
1268 );
1269 }
1270 }
1271
1272 #[test]
1273 fn zero_angle_with_drag_table_rejects_invalid_decks() {
1274 let inputs = valid_trajectory_inputs();
1275 let descending = [3.0, 2.0, 1.0, 0.5];
1276 unsafe {
1277 assert!(ballistics_calculate_zero_angle_with_drag_table(
1278 &inputs,
1279 std::ptr::null(),
1280 std::ptr::null(),
1281 100.0,
1282 std::ptr::null(),
1283 DECK_CD_LOW.as_ptr(),
1284 4,
1285 )
1286 .is_nan());
1287 assert!(ballistics_calculate_zero_angle_with_drag_table(
1288 &inputs,
1289 std::ptr::null(),
1290 std::ptr::null(),
1291 100.0,
1292 DECK_MACH.as_ptr(),
1293 DECK_CD_LOW.as_ptr(),
1294 0,
1295 )
1296 .is_nan());
1297 assert!(ballistics_calculate_zero_angle_with_drag_table(
1298 &inputs,
1299 std::ptr::null(),
1300 std::ptr::null(),
1301 100.0,
1302 descending.as_ptr(),
1303 DECK_CD_LOW.as_ptr(),
1304 4,
1305 )
1306 .is_nan());
1307 assert!(ballistics_calculate_zero_angle_with_drag_table(
1309 std::ptr::null(),
1310 std::ptr::null(),
1311 std::ptr::null(),
1312 100.0,
1313 DECK_MACH.as_ptr(),
1314 DECK_CD_LOW.as_ptr(),
1315 4,
1316 )
1317 .is_nan());
1318 }
1319 }
1320
1321 #[test]
1322 fn zero_then_fly_with_same_deck_is_consistent() {
1323 let mut inputs = valid_trajectory_inputs();
1327 unsafe {
1328 let angle = ballistics_calculate_zero_angle_with_drag_table(
1329 &inputs,
1330 std::ptr::null(),
1331 std::ptr::null(),
1332 100.0,
1333 DECK_MACH.as_ptr(),
1334 DECK_CD_LOW.as_ptr(),
1335 DECK_MACH.len() as c_int,
1336 );
1337 assert!(angle.is_finite());
1338 inputs.muzzle_angle = angle;
1339 let result = ballistics_calculate_trajectory_with_drag_table(
1340 &inputs,
1341 std::ptr::null(),
1342 std::ptr::null(),
1343 150.0,
1344 1.0,
1345 DECK_MACH.as_ptr(),
1346 DECK_CD_LOW.as_ptr(),
1347 DECK_MACH.len() as c_int,
1348 );
1349 assert!(!result.is_null());
1350 let zero_distance = 100.0;
1354 let pts = std::slice::from_raw_parts((*result).points, (*result).point_count as usize);
1355 let bracket = pts
1356 .windows(2)
1357 .find(|w| w[0].position_x <= zero_distance && w[1].position_x >= zero_distance)
1358 .expect("trajectory brackets the zero distance");
1359 let (lo, hi) = (&bracket[0], &bracket[1]);
1360 let y_at_zero = if hi.position_x > lo.position_x {
1361 let t = (zero_distance - lo.position_x) / (hi.position_x - lo.position_x);
1362 lo.position_y + t * (hi.position_y - lo.position_y)
1363 } else {
1364 lo.position_y
1365 };
1366 assert!(
1367 (y_at_zero - inputs.sight_height).abs() < 0.002,
1368 "zeroed flight missed the line of sight at 100 m: y={} (sight_height={})",
1369 y_at_zero,
1370 inputs.sight_height
1371 );
1372 ballistics_free_trajectory_result(result);
1373 }
1374 }
1375
1376 #[test]
1377 fn drag_table_len_above_cap_is_rejected() {
1378 let n = (MAX_FFI_DRAG_TABLE_LEN + 1) as usize;
1381 let mach: Vec<f64> = (0..n).map(|i| 0.01 + i as f64 * 0.001).collect();
1382 let cd: Vec<f64> = vec![0.3; n];
1383 let inputs = valid_trajectory_inputs();
1384 unsafe {
1385 let r = ballistics_calculate_trajectory_with_drag_table(
1386 &inputs,
1387 std::ptr::null(),
1388 std::ptr::null(),
1389 300.0,
1390 1.0,
1391 mach.as_ptr(),
1392 cd.as_ptr(),
1393 n as c_int,
1394 );
1395 assert!(r.is_null(), "len {n} must be rejected by the cap");
1396 }
1397 }
1398
1399 #[test]
1400 fn drag_table_len_at_cap_is_accepted() {
1401 let n = MAX_FFI_DRAG_TABLE_LEN as usize;
1402 let mach: Vec<f64> = (0..n).map(|i| 0.01 + i as f64 * 0.001).collect();
1403 let cd: Vec<f64> = vec![0.3; n];
1404 let inputs = valid_trajectory_inputs();
1405 unsafe {
1406 let r = ballistics_calculate_trajectory_with_drag_table(
1407 &inputs,
1408 std::ptr::null(),
1409 std::ptr::null(),
1410 300.0,
1411 1.0,
1412 mach.as_ptr(),
1413 cd.as_ptr(),
1414 n as c_int,
1415 );
1416 assert!(!r.is_null(), "len == cap must be accepted");
1417 ballistics_free_trajectory_result(r);
1418 }
1419 }
1420
1421 #[test]
1422 fn ffi_cant_angle_deflects_laterally() {
1423 let mut level = valid_trajectory_inputs();
1424 level.muzzle_angle = 0.003;
1425 let mut canted = valid_trajectory_inputs();
1426 canted.muzzle_angle = 0.003;
1427 canted.cant_angle = 10f64.to_radians();
1428 unsafe {
1429 let a = ballistics_calculate_trajectory(&level, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
1430 let b = ballistics_calculate_trajectory(&canted, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
1431 assert!(!a.is_null() && !b.is_null());
1432 let za = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_z;
1433 let zb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_z;
1434 assert!(zb > za + 0.005, "FFI cant must deflect right: level={za} canted={zb}");
1435 ballistics_free_trajectory_result(a);
1436 ballistics_free_trajectory_result(b);
1437 }
1438 }
1439
1440 #[test]
1441 fn ffi_vertical_wind_raises_trajectory() {
1442 let inputs = valid_trajectory_inputs();
1443 let no_wind = FFIWindConditions {
1444 speed: 0.0,
1445 direction: 0.0,
1446 vertical_speed: 0.0,
1447 };
1448 let updraft = FFIWindConditions {
1449 speed: 0.0,
1450 direction: 0.0,
1451 vertical_speed: 5.0,
1452 };
1453 unsafe {
1454 let a = ballistics_calculate_trajectory(&inputs, &no_wind, std::ptr::null(), 400.0, 1.0);
1455 let b = ballistics_calculate_trajectory(&inputs, &updraft, std::ptr::null(), 400.0, 1.0);
1456 assert!(!a.is_null() && !b.is_null());
1457 let ya = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_y;
1458 let yb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_y;
1459 assert!(yb > ya + 0.01, "FFI updraft must raise the trajectory: no_wind={ya} updraft={yb}");
1460 ballistics_free_trajectory_result(a);
1461 ballistics_free_trajectory_result(b);
1462 }
1463 }
1464}