diff --git a/code/cmdline/cmdline.cpp b/code/cmdline/cmdline.cpp index 7d3cd575ca1..2273b635b11 100644 --- a/code/cmdline/cmdline.cpp +++ b/code/cmdline/cmdline.cpp @@ -268,6 +268,7 @@ Flag exe_params[] = { "-reparse_mainhall", "Reparse mainhall.tbl when loading halls", false, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-reparse_mainhall", }, { "-noninteractive", "Disables interactive dialogs", true, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-noninteractive", }, { "-benchmark_mode", "Puts the game into benchmark mode", true, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-benchmark_mode", }, + { "-collision_bench", "Benchmark N frames of collision", true, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-collision_bench", }, { "-profile_frame_time","Profile frame time", true, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-profile_frame_time", }, { "-profile_write_file", "Write profiling information to file", true, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-profile_write_file", }, { "-json_profiling", "Generate JSON profiling output", true, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-json_profiling", }, @@ -522,6 +523,7 @@ cmdline_parm frame_profile_write_file("-profile_write_file", NULL, AT_NONE); // cmdline_parm no_unfocused_pause_arg("-no_unfocused_pause", NULL, AT_NONE); //Cmdline_no_unfocus_pause cmdline_parm retail_time_compression_range_arg("-orig_speedx_range", NULL, AT_NONE); //Cmdline_retail_time_compression_range cmdline_parm benchmark_mode_arg("-benchmark_mode", NULL, AT_NONE); //Cmdline_benchmark_mode +cmdline_parm collision_bench_arg("-collision_bench", NULL, AT_INT); //Cmdline_collision_bench cmdline_parm pilot_arg("-pilot", nullptr, AT_STRING); //Cmdline_pilot cmdline_parm noninteractive_arg("-noninteractive", NULL, AT_NONE); //Cmdline_noninteractive cmdline_parm json_profiling("-json_profiling", NULL, AT_NONE); //Cmdline_json_profiling @@ -564,6 +566,7 @@ bool Cmdline_profile_write_file = false; bool Cmdline_no_unfocus_pause = false; bool Cmdline_retail_time_compression_range = false; bool Cmdline_benchmark_mode = false; +int Cmdline_collision_bench = 0; const char *Cmdline_pilot = nullptr; bool Cmdline_noninteractive = false; bool Cmdline_json_profiling = false; @@ -2308,6 +2311,11 @@ bool SetCmdlineParams() Cmdline_benchmark_mode = true; } + if (collision_bench_arg.found()) + { + Cmdline_collision_bench = collision_bench_arg.get_int(); + } + if (pilot_arg.found()) { Cmdline_pilot = pilot_arg.str(); diff --git a/code/cmdline/cmdline.h b/code/cmdline/cmdline.h index c2c1c4911b3..4516abb31a8 100644 --- a/code/cmdline/cmdline.h +++ b/code/cmdline/cmdline.h @@ -147,6 +147,7 @@ extern bool Cmdline_profile_write_file; extern bool Cmdline_no_unfocus_pause; extern bool Cmdline_retail_time_compression_range; extern bool Cmdline_benchmark_mode; +extern int Cmdline_collision_bench; extern const char *Cmdline_pilot; extern bool Cmdline_noninteractive; extern bool Cmdline_json_profiling; diff --git a/code/debris/debris.cpp b/code/debris/debris.cpp index 7d45fa0ee79..22d0e0b4e79 100644 --- a/code/debris/debris.cpp +++ b/code/debris/debris.cpp @@ -916,7 +916,7 @@ void debris_hit(object *debris_obj, object * /*other_obj*/, vec3d *hitpos, float * NOTE: debris_hit_info pointer NULL for debris:weapon collision, otherwise debris:ship collision. * @return true if hit, else return false. */ -int debris_check_collision(object *pdebris, object *other_obj, vec3d *hitpos, collision_info_struct *debris_hit_info, vec3d* hitNormal) +int debris_check_collision(object *pdebris, object *other_obj, vec3d *hitpos, collision_info_struct *debris_hit_info, vec3d* hitNormal, mc_info* out_mc) { mc_info mc; @@ -956,9 +956,14 @@ int debris_check_collision(object *pdebris, object *other_obj, vec3d *hitpos, co } } - weapon *wp = &Weapons[other_obj->instance]; - wp->collisionInfo = new mc_info; // The weapon will free this memory later - *wp->collisionInfo = mc; + if (out_mc != nullptr) { + // Caller wants the result to stash on the weapon itself, on the main thread. + *out_mc = mc; + } else { + weapon *wp = &Weapons[other_obj->instance]; + wp->collisionInfo = new mc_info; // The weapon will free this memory later + *wp->collisionInfo = mc; + } return mc.num_hits; } diff --git a/code/debris/debris.h b/code/debris/debris.h index 7377f0e7fad..4a1961de639 100644 --- a/code/debris/debris.h +++ b/code/debris/debris.h @@ -70,6 +70,7 @@ typedef struct debris { extern SCP_vector Debris; struct collision_info_struct; +struct mc_info; void debris_init(); void debris_render(object * obj, model_draw_list *scene); @@ -88,7 +89,10 @@ void debris_create_set_velocity(const debris *db, const ship *source_shipp, cons // Fire scripting hook after debris creation void debris_create_fire_hook(object *obj, object *source_obj); -int debris_check_collision( object * obj, object * other_obj, vec3d * hitpos, collision_info_struct *debris_hit_info=NULL, vec3d* hitnormal = NULL ); +// out_mc, when given, receives the raw model collision result instead of it being stashed +// directly on the weapon. The threaded collision path needs that write deferred to the main +// thread, so the caller does it during post-processing. +int debris_check_collision( object * obj, object * other_obj, vec3d * hitpos, collision_info_struct *debris_hit_info=NULL, vec3d* hitnormal = NULL, mc_info* out_mc = nullptr ); void debris_hit( object * debris_obj, object * other_obj, vec3d * hitpos, float damage, vec3d* force ); void debris_add_to_hull_list(debris *db); diff --git a/code/model/modelcollide.cpp b/code/model/modelcollide.cpp index 4be050c368f..96d23c1fdb5 100644 --- a/code/model/modelcollide.cpp +++ b/code/model/modelcollide.cpp @@ -19,6 +19,7 @@ #include "model/model.h" #include "model/modelrender.h" #include "model/modelsinc.h" +#include "object/collideprofile.h" #include "render/3d.h" #include "tracing/Monitor.h" #include "tracing/tracing.h" @@ -306,6 +307,7 @@ static void mc_check_sphereline_face( int nv, vec3d ** verts, vec3d * plane_pnt, // check each edge to see if we hit, find the closest edge // Mc->hit_dist stores the best edge time of *all* faces float sphere_time; + COLLISION_PROF_INC(sphereline_edge_tests); if ( fvi_polyedge_sphereline(&hit_point, &Mc_p0, &Mc_direction, Mc->radius, nv, verts, &sphere_time)) { Assert( sphere_time >= 0.0f ); /* @@ -389,6 +391,8 @@ void model_collide_bsp_poly(bsp_collision_tree *tree, int leaf_index) vec3d *points[TMAP_MAX_VERTS]; while ( tested_leaf >= 0 ) { + COLLISION_PROF_INC(bsp_leaf_tests); + bsp_collision_leaf *leaf = &tree->leaf_list[tested_leaf]; bool flat_poly = false; @@ -439,6 +443,8 @@ void model_collide_bsp(bsp_collision_tree *tree, int node_index) return; } + COLLISION_PROF_INC(bsp_node_visits); + bsp_collision_node *node = &tree->node_list[node_index]; vec3d hitpos; @@ -1127,6 +1133,7 @@ int model_collide(mc_info *mc_info_obj) Mc = mc_info_obj; MONITOR_INC(NumFVI,1); + COLLISION_PROF_INC(model_collide_calls); Mc->num_hits = 0; // How many collisions were found Mc->shield_hit_tri = -1; // Assume we won't hit any shield polygons diff --git a/code/object/collidedebrisship.cpp b/code/object/collidedebrisship.cpp index 1400294d6e7..b80e01fe928 100644 --- a/code/object/collidedebrisship.cpp +++ b/code/object/collidedebrisship.cpp @@ -27,52 +27,28 @@ void calculate_ship_ship_collision_physics(collision_info_struct *ship_ship_hit_info); +// Narrowphase output for a debris/asteroid vs ship hit, carried from the worker thread to the +// main thread. +struct rock_ship_collision_data { + collision_info_struct hit_info; + vec3d hitpos; +}; + /** - * Checks debris-ship collisions. - * @param pair obj_pair pointer to the two objects. pair->a is debris and pair->b is ship. - * @return 1 if all future collisions between these can be ignored + * Applies a debris-ship collision. Main thread only: this mutates the physics and hull state of + * both objects. */ -int collide_debris_ship( obj_pair * pair ) +void collide_debris_ship_process( obj_pair * pair, const std::any& collision_data ) { - float dist; + auto cd = std::any_cast(collision_data); + collision_info_struct& debris_hit_info = cd.hit_info; + vec3d& hitpos = cd.hitpos; + object *debris_objp = pair->a; object *ship_objp = pair->b; - - // Don't check collisions for warping out player - if ( Player->control_mode != PCM_NORMAL ) { - if ( ship_objp == Player_obj ) - return 0; - } - - Assert( debris_objp->type == OBJ_DEBRIS ); - Assert( ship_objp->type == OBJ_SHIP ); - - if (reject_due_collision_groups(debris_objp, ship_objp)) - return 0; - ship* shipp = &Ships[ship_objp->instance]; - // don't check collision if it's our own debris and we are dying - if ( (debris_objp->parent == OBJ_INDEX(ship_objp)) && (shipp->flags[Ship::Ship_Flags::Dying]) ) - return 0; - dist = vm_vec_dist( &debris_objp->pos, &ship_objp->pos ); - if ( dist < debris_objp->radius + ship_objp->radius ) { - int hit; - vec3d hitpos; - // create and initialize ship_ship_hit_info struct - collision_info_struct debris_hit_info; - init_collision_info_struct(&debris_hit_info); - - if ( debris_objp->phys_info.mass > ship_objp->phys_info.mass ) { - debris_hit_info.heavy = debris_objp; - debris_hit_info.light = ship_objp; - } else { - debris_hit_info.heavy = ship_objp; - debris_hit_info.light = debris_objp; - } - - hit = debris_check_collision(debris_objp, ship_objp, &hitpos, &debris_hit_info ); - if ( hit ) + { { bool ship_override = false, debris_override = false; @@ -108,7 +84,7 @@ int collide_debris_ship( obj_pair * pair ) calculate_ship_ship_collision_physics( &debris_hit_info ); if ( debris_hit_info.impulse < 0.5f ) - return 0; + return; // calculate ship damage ship_damage = 0.005f * debris_hit_info.impulse; // Cut collision-based damage in half. @@ -193,9 +169,52 @@ int collide_debris_ship( obj_pair * pair ) scripting::hook_param("Hitpos", 'o', hitpos), scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (debris_hit_info.heavy == ship_objp)))); } + } + } +} - return 0; +/** + * Checks debris-ship collisions. Pure: safe to run on a collision worker thread. + * @param pair obj_pair pointer to the two objects. pair->a is debris and pair->b is ship. + */ +collision_result collide_debris_ship_check( obj_pair * pair ) +{ + float dist; + object *debris_objp = pair->a; + object *ship_objp = pair->b; + + // Don't check collisions for warping out player + if ( Player->control_mode != PCM_NORMAL ) { + if ( ship_objp == Player_obj ) + return { false, std::any(), &collide_debris_ship_process }; + } + + Assert( debris_objp->type == OBJ_DEBRIS ); + Assert( ship_objp->type == OBJ_SHIP ); + + if (reject_due_collision_groups(debris_objp, ship_objp)) + return { false, std::any(), &collide_debris_ship_process }; + + ship* shipp = &Ships[ship_objp->instance]; + // don't check collision if it's our own debris and we are dying + if ( (debris_objp->parent == OBJ_INDEX(ship_objp)) && (shipp->flags[Ship::Ship_Flags::Dying]) ) + return { false, std::any(), &collide_debris_ship_process }; + + dist = vm_vec_dist( &debris_objp->pos, &ship_objp->pos ); + if ( dist < debris_objp->radius + ship_objp->radius ) { + rock_ship_collision_data cd; + init_collision_info_struct(&cd.hit_info); + + if ( debris_objp->phys_info.mass > ship_objp->phys_info.mass ) { + cd.hit_info.heavy = debris_objp; + cd.hit_info.light = ship_objp; + } else { + cd.hit_info.heavy = ship_objp; + cd.hit_info.light = debris_objp; } + + if ( debris_check_collision(debris_objp, ship_objp, &cd.hitpos, &cd.hit_info ) ) + return { false, std::any(cd), &collide_debris_ship_process }; } else { // Bounding spheres don't intersect, set timestamp for next collision check. float ship_max_speed, debris_speed; float time; @@ -220,55 +239,35 @@ int collide_debris_ship( obj_pair * pair ) } } - return 0; + return { false, std::any(), &collide_debris_ship_process }; } -/** - * Checks asteroid-ship collisions. - * @param pair obj_pair pointer to the two objects. pair->a is asteroid and pair->b is ship. - * @return 1 if all future collisions between these can be ignored - */ -int collide_asteroid_ship( obj_pair * pair ) +int collide_debris_ship( obj_pair * pair ) { - if (!Asteroids_enabled) - return 0; - - float dist; - object *asteroid_objp = pair->a; - object *ship_objp = pair->b; + const auto& [never_check_again, collision_data, process_fnc] = collide_debris_ship_check(pair); - // Don't check collisions for warping out player - if ( Player->control_mode != PCM_NORMAL ) { - if ( ship_objp == Player_obj ) return 0; + if (collision_data.has_value()) { + process_fnc(pair, collision_data); } - if (asteroid_objp->hull_strength < 0.0f) - return 0; - - Assert( asteroid_objp->type == OBJ_ASTEROID ); - Assert( ship_objp->type == OBJ_SHIP ); + return never_check_again ? 1 : 0; +} - dist = vm_vec_dist( &asteroid_objp->pos, &ship_objp->pos ); +/** + * Applies an asteroid-ship collision. Main thread only: this mutates the physics and hull state + * of both objects. + */ +void collide_asteroid_ship_process( obj_pair * pair, const std::any& collision_data ) +{ + auto cd = std::any_cast(collision_data); + collision_info_struct& asteroid_hit_info = cd.hit_info; + vec3d& hitpos = cd.hitpos; + object *asteroid_objp = pair->a; + object *ship_objp = pair->b; ship* shipp = &Ships[ship_objp->instance]; - if ( dist < asteroid_objp->radius + ship_objp->radius ) { - int hit; - vec3d hitpos; - // create and initialize ship_ship_hit_info struct - collision_info_struct asteroid_hit_info; - init_collision_info_struct(&asteroid_hit_info); - - if ( asteroid_objp->phys_info.mass > ship_objp->phys_info.mass ) { - asteroid_hit_info.heavy = asteroid_objp; - asteroid_hit_info.light = ship_objp; - } else { - asteroid_hit_info.heavy = ship_objp; - asteroid_hit_info.light = asteroid_objp; - } - - hit = asteroid_check_collision(asteroid_objp, ship_objp, &hitpos, &asteroid_hit_info ); - if ( hit ) + { { bool ship_override = false, asteroid_override = false; @@ -306,7 +305,7 @@ int collide_asteroid_ship( obj_pair * pair ) calculate_ship_ship_collision_physics( &asteroid_hit_info ); if ( asteroid_hit_info.impulse < 0.5f ) - return 0; + return; // limit damage from impulse by making max impulse (for damage) 2*m*v_max_relative float max_ship_impulse = (2.0f*ship_objp->phys_info.max_vel.xyz.z+vm_vec_mag_quick(&asteroid_vel)) * @@ -380,11 +379,53 @@ int collide_asteroid_ship( obj_pair * pair ) scripting::hook_param("Hitpos", 'o', hitpos), scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (asteroid_hit_info.heavy == ship_objp)))); } + } + } +} - return 0; +/** + * Checks asteroid-ship collisions. Pure: safe to run on a collision worker thread. + * @param pair obj_pair pointer to the two objects. pair->a is asteroid and pair->b is ship. + */ +collision_result collide_asteroid_ship_check( obj_pair * pair ) +{ + if (!Asteroids_enabled) + return { false, std::any(), &collide_asteroid_ship_process }; + + float dist; + object *asteroid_objp = pair->a; + object *ship_objp = pair->b; + + // Don't check collisions for warping out player + if ( Player->control_mode != PCM_NORMAL ) { + if ( ship_objp == Player_obj ) + return { false, std::any(), &collide_asteroid_ship_process }; + } + + if (asteroid_objp->hull_strength < 0.0f) + return { false, std::any(), &collide_asteroid_ship_process }; + + Assert( asteroid_objp->type == OBJ_ASTEROID ); + Assert( ship_objp->type == OBJ_SHIP ); + + dist = vm_vec_dist( &asteroid_objp->pos, &ship_objp->pos ); + + ship* shipp = &Ships[ship_objp->instance]; + + if ( dist < asteroid_objp->radius + ship_objp->radius ) { + rock_ship_collision_data cd; + init_collision_info_struct(&cd.hit_info); + + if ( asteroid_objp->phys_info.mass > ship_objp->phys_info.mass ) { + cd.hit_info.heavy = asteroid_objp; + cd.hit_info.light = ship_objp; + } else { + cd.hit_info.heavy = ship_objp; + cd.hit_info.light = asteroid_objp; } - return 0; + if ( asteroid_check_collision(asteroid_objp, ship_objp, &cd.hitpos, &cd.hit_info ) ) + return { false, std::any(cd), &collide_asteroid_ship_process }; } else { // estimate earliest time at which pair can hit float asteroid_max_speed, ship_max_speed, time; @@ -409,8 +450,20 @@ int collide_asteroid_ship( obj_pair * pair ) } else { pair->next_check_time = timestamp(0); // check next time } - return 0; } + + return { false, std::any(), &collide_asteroid_ship_process }; +} + +int collide_asteroid_ship( obj_pair * pair ) +{ + const auto& [never_check_again, collision_data, process_fnc] = collide_asteroid_ship_check(pair); + + if (collision_data.has_value()) { + process_fnc(pair, collision_data); + } + + return never_check_again ? 1 : 0; } /** @@ -553,7 +606,7 @@ int collide_asteroid_prop(obj_pair* pair) asteroid_hit_info.light = asteroid_objp; } - hit = prop_check_collision(prop_objp, prop_objp, &hitpos, &asteroid_hit_info); + hit = prop_check_collision(prop_objp, asteroid_objp, &hitpos, &asteroid_hit_info); if (hit) { bool ship_override = false, asteroid_override = false; diff --git a/code/object/collidedebrisweapon.cpp b/code/object/collidedebrisweapon.cpp index d33426fdb30..bb508e03c37 100644 --- a/code/object/collidedebrisweapon.cpp +++ b/code/object/collidedebrisweapon.cpp @@ -21,31 +21,32 @@ +// Everything the narrowphase produces for a debris/asteroid vs weapon hit. The mc_info is +// carried across rather than being written straight onto the weapon, because that write has to +// happen on the main thread. +struct debris_weapon_collision_data { + vec3d hitpos; + vec3d hitnormal; + mc_info mc; +}; + /** - * Checks debris-weapon collisions. - * @param pair obj_pair pointer to the two objects. pair->a is debris and pair->b is weapon. - * @return 1 if all future collisions between these can be ignored + * Applies a debris-weapon collision. Main thread only. */ -int collide_debris_weapon( obj_pair * pair ) +void collide_debris_weapon_process( obj_pair * pair, const std::any& collision_data ) { - vec3d hitpos, hitnormal; + const auto& cd = std::any_cast(collision_data); + object *pdebris = pair->a; object *weapon_obj = pair->b; - Assert( pdebris->type == OBJ_DEBRIS ); - Assert( weapon_obj->type == OBJ_WEAPON ); + // mutable copies: the impact/hit helpers below take non-const vec3d* + vec3d hitpos = cd.hitpos; + vec3d hitnormal = cd.hitnormal; - if (reject_due_collision_groups(pdebris, weapon_obj)) - return 0; - - // first check the bounding spheres of the two objects. - int hit = fvi_segment_sphere(&hitpos, &weapon_obj->last_pos, &weapon_obj->pos, &pdebris->pos, pdebris->radius); - if (hit) { - hit = debris_check_collision(pdebris, weapon_obj, &hitpos, nullptr, &hitnormal ); - - if ( !hit ) - return 0; + Weapons[weapon_obj->instance].collisionInfo = new mc_info(cd.mc); // The weapon will free this memory later + { bool weapon_override = false, debris_override = false; if (scripting::hooks::OnDebrisCollision->isActive()) { @@ -101,40 +102,68 @@ int collide_debris_weapon( obj_pair * pair ) scripting::hook_param("Debris", 'o', pdebris), scripting::hook_param("Hitpos", 'o', hitpos))); } + } +} + +/** + * Checks debris-weapon collisions. Pure: safe to run on a collision worker thread. + * @param pair obj_pair pointer to the two objects. pair->a is debris and pair->b is weapon. + */ +collision_result collide_debris_weapon_check( obj_pair * pair ) +{ + object *pdebris = pair->a; + object *weapon_obj = pair->b; - return 0; + Assert( pdebris->type == OBJ_DEBRIS ); + Assert( weapon_obj->type == OBJ_WEAPON ); + if (reject_due_collision_groups(pdebris, weapon_obj)) + return { false, std::any(), &collide_debris_weapon_process }; + + debris_weapon_collision_data cd; + + // first check the bounding spheres of the two objects. + int hit = fvi_segment_sphere(&cd.hitpos, &weapon_obj->last_pos, &weapon_obj->pos, &pdebris->pos, pdebris->radius); + if (hit) { + hit = debris_check_collision(pdebris, weapon_obj, &cd.hitpos, nullptr, &cd.hitnormal, &cd.mc ); + + if ( !hit ) + return { false, std::any(), &collide_debris_weapon_process }; + + return { false, std::any(cd), &collide_debris_weapon_process }; } else { - return weapon_will_never_hit( weapon_obj, pdebris, pair ); + return { weapon_will_never_hit( weapon_obj, pdebris, pair ) != 0, std::any(), &collide_debris_weapon_process }; } -} +} + +int collide_debris_weapon( obj_pair * pair ) +{ + const auto& [never_check_again, collision_data, process_fnc] = collide_debris_weapon_check(pair); + + if (collision_data.has_value()) { + process_fnc(pair, collision_data); + } + + return never_check_again ? 1 : 0; +} /** - * Checks debris-weapon collisions. - * @param pair obj_pair pointer to the two objects. pair->a is debris and pair->b is weapon. - * @return 1 if all future collisions between these can be ignored + * Applies an asteroid-weapon collision. Main thread only. */ -int collide_asteroid_weapon( obj_pair * pair ) +void collide_asteroid_weapon_process( obj_pair * pair, const std::any& collision_data ) { - if (!Asteroids_enabled) - return 0; + const auto& cd = std::any_cast(collision_data); - vec3d hitpos, hitnormal; object *pasteroid = pair->a; object *weapon_obj = pair->b; - Assert( pasteroid->type == OBJ_ASTEROID); - Assert( weapon_obj->type == OBJ_WEAPON ); - - // first check the bounding spheres of the two objects. - int hit = fvi_segment_sphere(&hitpos, &weapon_obj->last_pos, &weapon_obj->pos, &pasteroid->pos, pasteroid->radius); - if (hit) { - hit = asteroid_check_collision(pasteroid, weapon_obj, &hitpos, nullptr, &hitnormal); - if ( !hit ) - return 0; + // mutable copies: the impact/hit helpers below take non-const vec3d* + vec3d hitpos = cd.hitpos; + vec3d hitnormal = cd.hitnormal; + { bool weapon_override = false, asteroid_override = false; if (scripting::hooks::OnAsteroidCollision->isActive()) { @@ -190,10 +219,46 @@ int collide_asteroid_weapon( obj_pair * pair ) scripting::hook_param("Asteroid", 'o', pasteroid), scripting::hook_param("Hitpos", 'o', hitpos))); } + } +} - return 0; +/** + * Checks asteroid-weapon collisions. Pure: safe to run on a collision worker thread. + * @param pair obj_pair pointer to the two objects. pair->a is asteroid and pair->b is weapon. + */ +collision_result collide_asteroid_weapon_check( obj_pair * pair ) +{ + if (!Asteroids_enabled) + return { false, std::any(), &collide_asteroid_weapon_process }; + object *pasteroid = pair->a; + object *weapon_obj = pair->b; + + Assert( pasteroid->type == OBJ_ASTEROID); + Assert( weapon_obj->type == OBJ_WEAPON ); + + debris_weapon_collision_data cd; + + // first check the bounding spheres of the two objects. + int hit = fvi_segment_sphere(&cd.hitpos, &weapon_obj->last_pos, &weapon_obj->pos, &pasteroid->pos, pasteroid->radius); + if (hit) { + hit = asteroid_check_collision(pasteroid, weapon_obj, &cd.hitpos, nullptr, &cd.hitnormal); + if ( !hit ) + return { false, std::any(), &collide_asteroid_weapon_process }; + + return { false, std::any(cd), &collide_asteroid_weapon_process }; } else { - return weapon_will_never_hit( weapon_obj, pasteroid, pair ); + return { weapon_will_never_hit( weapon_obj, pasteroid, pair ) != 0, std::any(), &collide_asteroid_weapon_process }; + } +} + +int collide_asteroid_weapon( obj_pair * pair ) +{ + const auto& [never_check_again, collision_data, process_fnc] = collide_asteroid_weapon_check(pair); + + if (collision_data.has_value()) { + process_fnc(pair, collision_data); } -} + + return never_check_again ? 1 : 0; +} diff --git a/code/object/collideprofile.cpp b/code/object/collideprofile.cpp new file mode 100644 index 00000000000..e63308a7529 --- /dev/null +++ b/code/object/collideprofile.cpp @@ -0,0 +1,149 @@ +#include "object/collideprofile.h" + +#include "cmdline/cmdline.h" +#include "gamesequence/gamesequence.h" +#include "utils/threading.h" + +#include +#include +#include + +namespace collision_profiling { + +counters& counters::operator+=(const counters& other) +{ + pair_calls += other.pair_calls; + pairs_considered += other.pairs_considered; + cache_size += other.cache_size; + pairs_cache_skipped += other.pairs_cache_skipped; + pairs_enqueued += other.pairs_enqueued; + pairs_checked_inline += other.pairs_checked_inline; + inline_beam += other.inline_beam; + inline_weapon_weapon += other.inline_weapon_weapon; + inline_debris_ship += other.inline_debris_ship; + inline_asteroid_ship += other.inline_asteroid_ship; + inline_prop += other.inline_prop; + inline_other += other.inline_other; + + model_collide_calls += other.model_collide_calls; + bsp_node_visits += other.bsp_node_visits; + bsp_leaf_tests += other.bsp_leaf_tests; + sphereline_edge_tests += other.sphereline_edge_tests; + + worker_idle_spins += other.worker_idle_spins; + drain_spins += other.drain_spins; + + collision_ns += other.collision_ns; + sort_ns += other.sort_ns; + overlap_ns += other.overlap_ns; + narrowphase_inline_ns += other.narrowphase_inline_ns; + drain_ns += other.drain_ns; + + return *this; +} + +thread_local counters Local = {}; + +static std::mutex Frame_mutex; +static counters Frame_total = {}; + +// accumulated across the measured window of a -collision_bench run +static counters Bench_total = {}; +static int Bench_frames_seen = 0; +static bool Bench_finished = false; + +// Mission load and page-in leave the first frames wildly unrepresentative, so throw some away +// before we start accumulating. +static const int BENCH_WARMUP_FRAMES = 120; + +void flush_local() +{ + std::scoped_lock lock(Frame_mutex); + Frame_total += Local; + Local = counters{}; +} + +static void reset_frame() +{ + std::scoped_lock lock(Frame_mutex); + Frame_total = counters{}; +} + +static void dump(const counters& c, int frames) +{ + if (frames < 1) { + frames = 1; + } + const auto per_frame = [frames](std::uint64_t total) { return static_cast(total) / frames; }; + + // Deliberately not mprintf: LoggingEnabled is false in NDEBUG builds without SCP_RELEASE_LOGGING + // (pstypes.h), and a release build is exactly where we need these numbers. + FILE* out = stdout; + fprintf(out, "\n=== collision benchmark: %d frames ===\n", frames); + fprintf(out, " threads : %d worker(s)\n", static_cast(threading::get_num_workers())); + fprintf(out, " collision phase : %.3f ms/frame (main thread, obj_sort_and_collide)\n", + per_frame(c.collision_ns) / 1000000.0); + fprintf(out, " sort passes : %.3f ms/frame\n", per_frame(c.sort_ns) / 1000000.0); + fprintf(out, " sweep passes : %.3f ms/frame (incl. inline narrowphase)\n", per_frame(c.overlap_ns) / 1000000.0); +#if COLLISION_PROFILING + // the split needs the per-pair narrowphase timer, which is part of the gated set + fprintf(out, " inline narrowph. : %.3f ms/frame\n", per_frame(c.narrowphase_inline_ns) / 1000000.0); + fprintf(out, " pair gen/cull : %.3f ms/frame\n", + (per_frame(c.overlap_ns) - per_frame(c.narrowphase_inline_ns)) / 1000000.0); +#endif + fprintf(out, " worker drain : %.3f ms/frame\n", per_frame(c.drain_ns) / 1000000.0); +#if !COLLISION_PROFILING + fprintf(out, " (event counters are compiled out; set COLLISION_PROFILING to 1 for them)\n"); + fprintf(out, "=== end collision benchmark ===\n\n"); + fflush(out); + return; +#else + fprintf(out, " -- broadphase --\n"); + fprintf(out, " obj_collide_pair call: %.1f /frame\n", per_frame(c.pair_calls)); + fprintf(out, " pairs considered : %.1f /frame (%" PRIu64 " total)\n", per_frame(c.pairs_considered), c.pairs_considered); + fprintf(out, " pair cache entries : %.0f (mean over the window)\n", per_frame(c.cache_size)); + fprintf(out, " cache skipped : %.1f /frame\n", per_frame(c.pairs_cache_skipped)); + fprintf(out, " enqueued to workers : %.1f /frame\n", per_frame(c.pairs_enqueued)); + fprintf(out, " checked inline : %.1f /frame\n", per_frame(c.pairs_checked_inline)); + fprintf(out, " beam : %.1f /frame\n", per_frame(c.inline_beam)); + fprintf(out, " weapon<->weapon : %.1f /frame\n", per_frame(c.inline_weapon_weapon)); + fprintf(out, " debris<->ship : %.1f /frame\n", per_frame(c.inline_debris_ship)); + fprintf(out, " asteroid<->ship : %.1f /frame\n", per_frame(c.inline_asteroid_ship)); + fprintf(out, " prop : %.1f /frame\n", per_frame(c.inline_prop)); + fprintf(out, " other : %.1f /frame\n", per_frame(c.inline_other)); + fprintf(out, " -- narrowphase --\n"); + fprintf(out, " model_collide calls : %.1f /frame\n", per_frame(c.model_collide_calls)); + fprintf(out, " BSP node visits : %.1f /frame (%" PRIu64 " total)\n", per_frame(c.bsp_node_visits), c.bsp_node_visits); + fprintf(out, " BSP leaf poly tests : %.1f /frame\n", per_frame(c.bsp_leaf_tests)); + fprintf(out, " sphereline edge tests: %.1f /frame\n", per_frame(c.sphereline_edge_tests)); + fprintf(out, " -- threading --\n"); + fprintf(out, " worker idle spins : %.1f /frame (%" PRIu64 " total)\n", per_frame(c.worker_idle_spins), c.worker_idle_spins); + fprintf(out, " drain spins : %.1f /frame (%" PRIu64 " total)\n", per_frame(c.drain_spins), c.drain_spins); + fprintf(out, "=== end collision benchmark ===\n\n"); + fflush(out); +#endif +} + +void benchmark_frame() +{ + if (Cmdline_collision_bench <= 0 || Bench_finished) { + reset_frame(); + return; + } + + ++Bench_frames_seen; + + if (Bench_frames_seen > BENCH_WARMUP_FRAMES) { + Bench_total += Frame_total; + + if (Bench_frames_seen - BENCH_WARMUP_FRAMES >= Cmdline_collision_bench) { + dump(Bench_total, Cmdline_collision_bench); + Bench_finished = true; + gameseq_post_event(GS_EVENT_QUIT_GAME); + } + } + + reset_frame(); +} + +} diff --git a/code/object/collideprofile.h b/code/object/collideprofile.h new file mode 100644 index 00000000000..c0aa9a0e458 --- /dev/null +++ b/code/object/collideprofile.h @@ -0,0 +1,94 @@ +#pragma once + +#include + +/** @file + * Lightweight instrumentation for the collision subsystem. + * + * The narrowphase runs on the collision worker threads, and the tracing system cannot be used + * there: tracing::complete::start/end bump a plain (non-atomic) `current_id` and hand events to + * FrameProfiler, which latches a main thread id from whichever event reaches it first. So the + * counters here live in thread-local storage instead, are incremented without atomics, and are + * folded into a shared per-frame total once per thread per frame. + * + * Counts, not timings, are the primary signal. A BSP node visit count is deterministic and + * machine independent, which makes it a far better regression test for a traversal optimization + * than a wall clock number taken from a mission that is never reproducible frame for frame. + * + * Two levels of detail: + * - the per-frame phase timers and the -collision_bench driver are always compiled in; they + * cost a handful of clock reads per frame, which is not measurable. + * - the per-pair and per-BSP-node counters are hot enough to perturb what they measure, so + * they are gated on COLLISION_PROFILING and are off by default. Set it to 1 to get the + * counts. Measured cost on bp2-massivebattle: 4.29 ms/frame with the counters against + * 4.07 without, i.e. about 5% of the collision phase, nearly all of it the per-pair + * narrowphase timer. + */ + +#define COLLISION_PROFILING 0 + +namespace collision_profiling { + +struct counters { + // broadphase + std::uint64_t pair_calls; // every obj_collide_pair call, including the ones rejected up front + std::uint64_t pairs_considered; // obj_collide_pair calls that got as far as the type dispatch + std::uint64_t cache_size; // Collision_cached_pairs entry count, sampled once per frame + std::uint64_t pairs_cache_skipped; // rejected by the Collision_cached_pairs timestamp + std::uint64_t pairs_enqueued; // handed to a worker thread + std::uint64_t pairs_checked_inline; // narrowphased on the main thread + + // breakdown of pairs_checked_inline, so it is visible which types are still costing us + std::uint64_t inline_beam; + std::uint64_t inline_weapon_weapon; + std::uint64_t inline_debris_ship; + std::uint64_t inline_asteroid_ship; + std::uint64_t inline_prop; + std::uint64_t inline_other; + + // narrowphase + std::uint64_t model_collide_calls; + std::uint64_t bsp_node_visits; // model_collide_bsp entries + std::uint64_t bsp_leaf_tests; // polys pulled out of a leaf chain + std::uint64_t sphereline_edge_tests; // fvi_polyedge_sphereline calls, the expensive branch + + // threading + std::uint64_t worker_idle_spins; // worker loop iterations with nothing to do + std::uint64_t drain_spins; // post_process_threaded_collisions loop iterations + + // main thread wall time, nanoseconds + std::uint64_t collision_ns; // all of obj_sort_and_collide + std::uint64_t sort_ns; // the three quicksort passes + std::uint64_t overlap_ns; // the three sweep passes (includes obj_collide_pair + inline narrowphase) + std::uint64_t narrowphase_inline_ns; // the check_collision calls made on the main thread + std::uint64_t drain_ns; // post_process_threaded_collisions + + counters& operator+=(const counters& other); +}; + +extern thread_local counters Local; + +//! Fold this thread's counters into the frame total and reset them. Called once per thread per frame. +void flush_local(); + +/** + * @brief Per-frame benchmark driver, hooked into game_do_frame(). + * + * Does nothing unless -collision_bench was passed. Skips a warmup window, accumulates the + * requested number of frames, writes a summary to stdout and quits the game. + */ +void benchmark_frame(); + +//! Once-per-frame accounting; always compiled in. +#define COLLISION_PROF_FRAME_ADD(field, n) (::collision_profiling::Local.field += (n)) + +#if COLLISION_PROFILING +//! Per-pair / per-node accounting; hot enough to perturb the measurement, so it is opt-in. +#define COLLISION_PROF_INC(field) (++::collision_profiling::Local.field) +#define COLLISION_PROF_ADD(field, n) (::collision_profiling::Local.field += (n)) +#else +#define COLLISION_PROF_INC(field) ((void)0) +#define COLLISION_PROF_ADD(field, n) ((void)0) +#endif + +} diff --git a/code/object/collideweaponweapon.cpp b/code/object/collideweaponweapon.cpp index 04e49bec114..07cd2e7ce54 100644 --- a/code/object/collideweaponweapon.cpp +++ b/code/object/collideweaponweapon.cpp @@ -22,84 +22,28 @@ #include "weapon/weapon.h" +// The only thing the narrowphase produces here is the facing dot product, which the damage +// curves need. Everything else the process step can re-derive from the two objects. +struct weapon_weapon_collision_data { + float dot; +}; + /** - * Checks weapon-weapon collisions. - * @param pair obj_pair pointer to the two objects. pair->a and pair->b are weapons. - * @return 1 if all future collisions between these can be ignored + * Applies a weapon-weapon collision. Main thread only. */ -int collide_weapon_weapon( obj_pair * pair ) +void collide_weapon_weapon_process( obj_pair * pair, const std::any& collision_data ) { - float A_radius, B_radius; + const auto& cd = std::any_cast(collision_data); + const float dot = cd.dot; + object *A = pair->a; object *B = pair->b; - Assert( A->type == OBJ_WEAPON ); - Assert( B->type == OBJ_WEAPON ); - - // Don't allow ship to shoot down its own missile. - if (A->parent_sig == B->parent_sig) - return 1; - - float dot = vm_vec_dot(&A->orient.vec.fvec, &B->orient.vec.fvec); - - // Only shoot down teammate's missile if not traveling in nearly same direction. - if (Weapons[A->instance].team == Weapons[B->instance].team) - if (dot > 0.7f) - return 1; - - // Ignore collisions involving a bomb if the bomb is not yet armed. - weapon *wpA, *wpB; - weapon_info *wipA, *wipB; - - wpA = &Weapons[A->instance]; - wpB = &Weapons[B->instance]; - wipA = &Weapon_info[wpA->weapon_info_index]; - wipB = &Weapon_info[wpB->weapon_info_index]; - - A_radius = A->radius; - B_radius = B->radius; - - float A_time_alive = f2fl(Missiontime - wpA->creation_time); - float B_time_alive = f2fl(Missiontime - wpB->creation_time); - - if (wipA->weapon_hitpoints > 0) { - if (!(wipA->wi_flags[Weapon::Info_Flags::No_radius_doubling])) { - A_radius *= 2; // Makes bombs easier to hit - } - - // the erroneous extra time a bomb stays invulnerable without the fix - float extra_buggy_time = 0.0f; - if (!(The_mission.ai_profile->flags[AI::Profile_Flags::Aspect_invulnerability_fix]) && wipA->is_locked_homing()) - extra_buggy_time = (wipA->lifetime * LOCKED_HOMING_EXTENDED_LIFE_FACTOR) - wipA->lifetime; - - if ((The_mission.ai_profile->flags[AI::Profile_Flags::Aspect_invulnerability_fix]) && (wipA->is_locked_homing()) && (wpA->homing_object != &obj_used_list)) { - if (A_time_alive < The_mission.ai_profile->delay_bomb_arm_timer[Game_skill_level] ) - return 0; - } - else if (A_time_alive - extra_buggy_time < The_mission.ai_profile->delay_bomb_arm_timer[Game_skill_level] ) - return 0; - } - - if (wipB->weapon_hitpoints > 0) { - if (!(wipB->wi_flags[Weapon::Info_Flags::No_radius_doubling])) { - B_radius *= 2; // Makes bombs easier to hit - } + weapon *wpA = &Weapons[A->instance]; + weapon *wpB = &Weapons[B->instance]; + weapon_info *wipA = &Weapon_info[wpA->weapon_info_index]; + weapon_info *wipB = &Weapon_info[wpB->weapon_info_index]; - // the erroneous extra time a bomb stays invulnerable without the fix - float extra_buggy_time = 0.0f; - if (!(The_mission.ai_profile->flags[AI::Profile_Flags::Aspect_invulnerability_fix]) && wipB->is_locked_homing()) - extra_buggy_time = (wipB->lifetime * LOCKED_HOMING_EXTENDED_LIFE_FACTOR) - wipB->lifetime; - - if ((The_mission.ai_profile->flags[AI::Profile_Flags::Aspect_invulnerability_fix]) && (wipB->is_locked_homing()) && (wpB->homing_object != &obj_used_list)) { - if (B_time_alive < The_mission.ai_profile->delay_bomb_arm_timer[Game_skill_level] ) - return 0; - } - else if (B_time_alive - extra_buggy_time < The_mission.ai_profile->delay_bomb_arm_timer[Game_skill_level] ) - return 0; - } - - // Rats, do collision detection. - if (collide_subdivide(&A->last_pos, &A->pos, A_radius, &B->last_pos, &B->pos, B_radius)) { bool a_override = false, b_override = false; @@ -277,7 +221,7 @@ int collide_weapon_weapon( obj_pair * pair ) } if (!scripting::hooks::OnWeaponCollision->isActive()) { - return 1; + return; } if(!(b_override && !a_override)) @@ -299,9 +243,98 @@ int collide_weapon_weapon( obj_pair * pair ) scripting::hook_param("WeaponB", 'o', A), scripting::hook_param("Hitpos", 'o', A->pos))); } + } +} + +/** + * Checks weapon-weapon collisions. Pure: safe to run on a collision worker thread. + * @param pair obj_pair pointer to the two objects. pair->a and pair->b are weapons. + */ +collision_result collide_weapon_weapon_check( obj_pair * pair ) +{ + float A_radius, B_radius; + object *A = pair->a; + object *B = pair->b; + + Assert( A->type == OBJ_WEAPON ); + Assert( B->type == OBJ_WEAPON ); + + // Don't allow ship to shoot down its own missile. + if (A->parent_sig == B->parent_sig) + return { true, std::any(), &collide_weapon_weapon_process }; + + float dot = vm_vec_dot(&A->orient.vec.fvec, &B->orient.vec.fvec); + + // Only shoot down teammate's missile if not traveling in nearly same direction. + if (Weapons[A->instance].team == Weapons[B->instance].team) + if (dot > 0.7f) + return { true, std::any(), &collide_weapon_weapon_process }; + + // Ignore collisions involving a bomb if the bomb is not yet armed. + weapon *wpA, *wpB; + weapon_info *wipA, *wipB; + + wpA = &Weapons[A->instance]; + wpB = &Weapons[B->instance]; + wipA = &Weapon_info[wpA->weapon_info_index]; + wipB = &Weapon_info[wpB->weapon_info_index]; + + A_radius = A->radius; + B_radius = B->radius; + + float A_time_alive = f2fl(Missiontime - wpA->creation_time); + float B_time_alive = f2fl(Missiontime - wpB->creation_time); + + if (wipA->weapon_hitpoints > 0) { + if (!(wipA->wi_flags[Weapon::Info_Flags::No_radius_doubling])) { + A_radius *= 2; // Makes bombs easier to hit + } + + // the erroneous extra time a bomb stays invulnerable without the fix + float extra_buggy_time = 0.0f; + if (!(The_mission.ai_profile->flags[AI::Profile_Flags::Aspect_invulnerability_fix]) && wipA->is_locked_homing()) + extra_buggy_time = (wipA->lifetime * LOCKED_HOMING_EXTENDED_LIFE_FACTOR) - wipA->lifetime; + + if ((The_mission.ai_profile->flags[AI::Profile_Flags::Aspect_invulnerability_fix]) && (wipA->is_locked_homing()) && (wpA->homing_object != &obj_used_list)) { + if (A_time_alive < The_mission.ai_profile->delay_bomb_arm_timer[Game_skill_level] ) + return { false, std::any(), &collide_weapon_weapon_process }; + } + else if (A_time_alive - extra_buggy_time < The_mission.ai_profile->delay_bomb_arm_timer[Game_skill_level] ) + return { false, std::any(), &collide_weapon_weapon_process }; + } + + if (wipB->weapon_hitpoints > 0) { + if (!(wipB->wi_flags[Weapon::Info_Flags::No_radius_doubling])) { + B_radius *= 2; // Makes bombs easier to hit + } + + // the erroneous extra time a bomb stays invulnerable without the fix + float extra_buggy_time = 0.0f; + if (!(The_mission.ai_profile->flags[AI::Profile_Flags::Aspect_invulnerability_fix]) && wipB->is_locked_homing()) + extra_buggy_time = (wipB->lifetime * LOCKED_HOMING_EXTENDED_LIFE_FACTOR) - wipB->lifetime; + + if ((The_mission.ai_profile->flags[AI::Profile_Flags::Aspect_invulnerability_fix]) && (wipB->is_locked_homing()) && (wpB->homing_object != &obj_used_list)) { + if (B_time_alive < The_mission.ai_profile->delay_bomb_arm_timer[Game_skill_level] ) + return { false, std::any(), &collide_weapon_weapon_process }; + } + else if (B_time_alive - extra_buggy_time < The_mission.ai_profile->delay_bomb_arm_timer[Game_skill_level] ) + return { false, std::any(), &collide_weapon_weapon_process }; + } + + // Rats, do collision detection. + if (collide_subdivide(&A->last_pos, &A->pos, A_radius, &B->last_pos, &B->pos, B_radius)) + return { true, std::any(weapon_weapon_collision_data{dot}), &collide_weapon_weapon_process }; + + return { false, std::any(), &collide_weapon_weapon_process }; +} + +int collide_weapon_weapon( obj_pair * pair ) +{ + const auto& [never_check_again, collision_data, process_fnc] = collide_weapon_weapon_check(pair); - return 1; + if (collision_data.has_value()) { + process_fnc(pair, collision_data); } - return 0; + return never_check_again ? 1 : 0; } diff --git a/code/object/objcollide.cpp b/code/object/objcollide.cpp index 5b2ffc575b2..8b806ca1c4d 100644 --- a/code/object/objcollide.cpp +++ b/code/object/objcollide.cpp @@ -11,6 +11,7 @@ #include "cmdline/cmdline.h" #include "globalincs/linklist.h" #include "io/timer.h" +#include "object/collideprofile.h" #include "object/objcollide.h" #include "object/object.h" #include "object/objectdock.h" @@ -21,7 +22,9 @@ #include "tracing/Monitor.h" #include "utils/threading.h" +#include #include +#include // the next 2 variables are used for pair statistics @@ -33,25 +36,154 @@ SCP_vector Collision_sort_list; static_assert(1 << collision_cache_bitshift > MAX_OBJECTS, "Collision pair caching currently relies on the highest possible objnum being less than 2^collision_cache_bitshift."); -class collider_pair +constexpr uint collision_cache_key_mask = (1u << collision_cache_bitshift) - 1; + +// The two objects of a cached pair are implied by the key -- it is built from their objnums -- +// so the entry only has to remember which *incarnation* of those two slots it was built for. +struct collider_pair { -public: - object *a; - object *b; int signature_a; int signature_b; int next_check_time; - bool initialized; +}; + +static int collision_pair_objnum_a(uint key) { return static_cast(key >> collision_cache_bitshift); } +static int collision_pair_objnum_b(uint key) { return static_cast(key & collision_cache_key_mask); } + +/** + * @brief Open-addressed hash table mapping a packed objnum pair to its cached collision state. + * + * This used to be an SCP_unordered_map, which is node based: every entry is its own heap + * allocation, so a lookup costs a miss on the bucket array and then a second miss chasing the + * node pointer. A heavy mission keeps a quarter of a million pairs alive, which made this + * lookup alone roughly 44% of all collision detection time. Linear probing over a flat array + * of 16-byte entries keeps the common case to a single cache line. + * + * Key 0 is used as the empty sentinel. That is safe because key 0 means "objnum 0 against + * objnum 0", and obj_collide_pair rejects A == B before it ever builds a key. + */ +class collider_pair_cache +{ + struct slot { + uint key; + collider_pair val; + }; + + SCP_vector _slots; + size_t _mask = 0; + size_t _count = 0; + + // Fibonacci hashing. The low bits of the key are objnum b, which is far from uniform, so + // mix the whole key and use the high bits of the product. + static size_t hash_key(uint key) + { + return static_cast((static_cast(key) * 0x9E3779B97F4A7C15ull) >> 32); + } + + void rehash(size_t new_capacity) + { + SCP_vector old; + old.swap(_slots); + + _slots.assign(new_capacity, slot{0, collider_pair{}}); + _mask = new_capacity - 1; + _count = 0; + + for (const auto& s : old) { + if (s.key != 0) { + insert_unchecked(s.key, s.val); + } + } + } + + void insert_unchecked(uint key, const collider_pair& val) + { + size_t i = hash_key(key) & _mask; + while (_slots[i].key != 0) { + i = (i + 1) & _mask; + } + _slots[i].key = key; + _slots[i].val = val; + ++_count; + } + +public: + collider_pair_cache() { rehash(1024); } + + size_t size() const { return _count; } + + void clear() + { + _slots.assign(_slots.size(), slot{0, collider_pair{}}); + _count = 0; + } + + /** + * @brief Find the entry for @a key, creating a default one if it is not present. + * @param[out] existed set to true if the entry was already in the table + * + * The returned pointer is invalidated by any subsequent insertion. + */ + collider_pair* get_or_create(uint key, bool& existed) + { + size_t i = hash_key(key) & _mask; + while (_slots[i].key != 0) { + if (_slots[i].key == key) { + existed = true; + return &_slots[i].val; + } + i = (i + 1) & _mask; + } + + // Keep the load factor at or below 1/2; linear probing degrades badly past that. + if ((_count + 1) * 2 > _slots.size()) { + rehash(_slots.size() * 2); + existed = false; + // re-probe in the grown table + size_t j = hash_key(key) & _mask; + while (_slots[j].key != 0) { + j = (j + 1) & _mask; + } + _slots[j].key = key; + _slots[j].val = collider_pair{-1, -1, -1}; + ++_count; + return &_slots[j].val; + } + + existed = false; + _slots[i].key = key; + _slots[i].val = collider_pair{-1, -1, -1}; + ++_count; + return &_slots[i].val; + } + + /** + * @brief Visit every live entry, dropping the ones @a pred rejects. + * + * Implemented as a compacting rebuild rather than in-place erase: linear probing needs + * either tombstones or backward shifting to erase safely, and this is only ever called + * while already walking the whole table. + */ + template + void retain(Pred pred) + { + SCP_vector old; + old.swap(_slots); - // we need to define a constructor because the hash map can - // implicitly insert an object when we use the [] operator - collider_pair() - : a(nullptr), b(nullptr), signature_a(-1), signature_b(-1), next_check_time(-1), initialized(false) - {} + _slots.assign(old.size(), slot{0, collider_pair{}}); + _mask = old.size() - 1; + _count = 0; + + for (auto& s : old) { + if (s.key != 0 && pred(s.key, s.val)) { + insert_unchecked(s.key, s.val); + } + } + } }; static SCP_set Collision_cache_stale_objects; -static SCP_unordered_map Collision_cached_pairs; +static collider_pair_cache Collision_cached_pairs; class checkobject; extern checkobject CheckObjects[MAX_OBJECTS]; @@ -511,29 +643,31 @@ int collide_remove_weapons( ) } // first pass is to see if any of the weapons don't have collision pairs. - for (auto& pair : Collision_cached_pairs) { - collider_pair* pair_obj = &pair.second; - - if (!pair_obj->initialized) { - continue; - } - - if (pair_obj->a->type == OBJ_WEAPON && pair_obj->signature_a == pair_obj->a->signature) { - crw_check_weapon(pair_obj->a->instance, pair_obj->next_check_time); - - if (crw_status[pair_obj->a->instance] == CRW_CAN_DELETE) { - pair_obj->initialized = false; + // Dropping the entry here replaces the old "initialized = false" reset: either way the next + // lookup of this key starts from scratch. + Collision_cached_pairs.retain([](uint key, collider_pair& pair) { + object* a = &Objects[collision_pair_objnum_a(key)]; + object* b = &Objects[collision_pair_objnum_b(key)]; + bool keep = true; + + if (a->type == OBJ_WEAPON && pair.signature_a == a->signature) { + crw_check_weapon(a->instance, pair.next_check_time); + + if (crw_status[a->instance] == CRW_CAN_DELETE) { + keep = false; } } - if (pair_obj->b->type == OBJ_WEAPON && pair_obj->signature_b == pair_obj->b->signature) { - crw_check_weapon(pair_obj->b->instance, pair_obj->next_check_time); + if (b->type == OBJ_WEAPON && pair.signature_b == b->signature) { + crw_check_weapon(b->instance, pair.next_check_time); - if (crw_status[pair_obj->b->instance] == CRW_CAN_DELETE) { - pair_obj->initialized = false; + if (crw_status[b->instance] == CRW_CAN_DELETE) { + keep = false; } } - } + + return keep; + }); // for each weapon which could be removed, delete the object int num_deleted = 0; @@ -617,17 +751,19 @@ void obj_collide_retime_stale_pairs() { TRACE_SCOPE(tracing::RetimeCollisionCache); - auto it = Collision_cached_pairs.begin(); - while (it != Collision_cached_pairs.end()) { - auto &pair = it->second; - if (pair.signature_a != pair.a->signature || pair.signature_b != pair.b->signature) { - it = Collision_cached_pairs.erase(it); - } else { - if (pair.a->flags[Object::Object_Flags::Collision_cache_stale] || pair.b->flags[Object::Object_Flags::Collision_cache_stale]) - pair.next_check_time = timestamp(0); - it++; - } - } + Collision_cached_pairs.retain([](uint key, collider_pair& pair) { + object* a = &Objects[collision_pair_objnum_a(key)]; + object* b = &Objects[collision_pair_objnum_b(key)]; + + // either slot has been recycled since this entry was made, so it is dead weight + if (pair.signature_a != a->signature || pair.signature_b != b->signature) + return false; + + if (a->flags[Object::Object_Flags::Collision_cache_stale] || b->flags[Object::Object_Flags::Collision_cache_stale]) + pair.next_check_time = timestamp(0); + + return true; + }); for (auto objp : Collision_cache_stale_objects) objp->flags.remove(Object::Object_Flags::Collision_cache_stale); @@ -644,53 +780,60 @@ void obj_collide_obj_cache_stale(object* objp) namespace { -float obj_get_collider_endpoint(int obj_num, int axis, bool min) +// Endpoints for the axis currently being swept, indexed by objnum. The sort and overlap passes +// between them ask for an endpoint hundreds of thousands of times a frame, and recomputing it +// every time meant an Objects[] indirection plus a three-way type dispatch each time. Computing +// both ends once per object per axis turns all of that into an array read. +static SCP_vector Collider_endpoint_min; +static SCP_vector Collider_endpoint_max; + +void obj_compute_collider_endpoints(int obj_num, int axis, float *min_out, float *max_out) { - if ( Objects[obj_num].type == OBJ_BEAM ) { - beam *b = &Beams[Objects[obj_num].instance]; + const object *objp = &Objects[obj_num]; + + if ( objp->type == OBJ_BEAM ) { + const beam *b = &Beams[objp->instance]; // use the last start and last shot as endpoints - float min_end, max_end; if ( b->last_start.a1d[axis] > b->last_shot.a1d[axis] ) { - min_end = b->last_shot.a1d[axis]; - max_end = b->last_start.a1d[axis]; - } else { - min_end = b->last_start.a1d[axis]; - max_end = b->last_shot.a1d[axis]; - } - - if ( min ) { - return min_end; - } else { - return max_end; - } - } else if ( Objects[obj_num].type == OBJ_WEAPON ) { - float min_end, max_end; - - if ( Objects[obj_num].pos.a1d[axis] > Objects[obj_num].last_pos.a1d[axis] ) { - min_end = Objects[obj_num].last_pos.a1d[axis]; - max_end = Objects[obj_num].pos.a1d[axis]; + *min_out = b->last_shot.a1d[axis]; + *max_out = b->last_start.a1d[axis]; } else { - min_end = Objects[obj_num].pos.a1d[axis]; - max_end = Objects[obj_num].last_pos.a1d[axis]; + *min_out = b->last_start.a1d[axis]; + *max_out = b->last_shot.a1d[axis]; } - - if ( min ) { - return min_end - Objects[obj_num].radius; + } else if ( objp->type == OBJ_WEAPON ) { + if ( objp->pos.a1d[axis] > objp->last_pos.a1d[axis] ) { + *min_out = objp->last_pos.a1d[axis] - objp->radius; + *max_out = objp->pos.a1d[axis] + objp->radius; } else { - return max_end + Objects[obj_num].radius; + *min_out = objp->pos.a1d[axis] - objp->radius; + *max_out = objp->last_pos.a1d[axis] + objp->radius; } } else { - vec3d *pos = &Objects[obj_num].pos; + *min_out = objp->pos.a1d[axis] - objp->radius; + *max_out = objp->pos.a1d[axis] + objp->radius; + } +} - if ( min ) { - return pos->a1d[axis] - Objects[obj_num].radius; - } else { - return pos->a1d[axis] + Objects[obj_num].radius; - } +//! Refresh the endpoint cache for @a axis, for exactly the objects in @a list. +void obj_cache_collider_endpoints(const SCP_vector &list, int axis) +{ + if ( Collider_endpoint_min.size() < static_cast(MAX_OBJECTS) ) { + Collider_endpoint_min.resize(MAX_OBJECTS); + Collider_endpoint_max.resize(MAX_OBJECTS); + } + + for (int obj_num : list) { + obj_compute_collider_endpoints(obj_num, axis, &Collider_endpoint_min[obj_num], &Collider_endpoint_max[obj_num]); } } +inline float obj_get_collider_endpoint(int obj_num, bool min) +{ + return min ? Collider_endpoint_min[obj_num] : Collider_endpoint_max[obj_num]; +} + void obj_quicksort_colliders(SCP_vector *list, int left, int right, int axis) { Assert( axis >= 0 ); @@ -699,7 +842,7 @@ void obj_quicksort_colliders(SCP_vector *list, int left, int right, int axi if ( right > left ) { int pivot_index = left + (right - left) / 2; - float pivot_value = obj_get_collider_endpoint((*list)[pivot_index], axis, true); + float pivot_value = obj_get_collider_endpoint((*list)[pivot_index], true); // swap! int temp = (*list)[pivot_index]; @@ -709,7 +852,7 @@ void obj_quicksort_colliders(SCP_vector *list, int left, int right, int axi int store_index = left; for (int i = left; i < right; ++i ) { - if ( obj_get_collider_endpoint((*list)[i], axis, true) <= pivot_value ) { + if ( obj_get_collider_endpoint((*list)[i], true) <= pivot_value ) { temp = (*list)[i]; (*list)[i] = (*list)[store_index]; (*list)[store_index] = temp; @@ -740,6 +883,7 @@ struct collision_thread_data { std::atomic_size_t queue_length, result_length; std::mutex queue_mutex, result_mutex; + std::condition_variable work_available; std::unique_ptr> queue_load, queue_process; std::unique_ptr> queue_results, queue_send; @@ -755,6 +899,10 @@ struct collision_thread_data { std::unique_ptr collision_thread_data_buffer; std::atomic_bool collision_processing_done = false; +// Results gathered from the workers during the drain, applied once they have all stopped. +// Kept at file scope so the allocation is reused frame to frame. +SCP_vector collision_pending_results; + void spin_up_mp_collision() { collision_processing_done.store(false); threading::spin_up_threaded_task(threading::WorkerThreadTask::COLLISION); @@ -762,38 +910,78 @@ void spin_up_mp_collision() { void spin_down_mp_collision() { threading::spin_down_threaded_task(); - collision_processing_done.store(true); - threading::spin_down_wait_complete(); -} + collision_processing_done.store(true, std::memory_order_release); -void queue_mp_collision(uint ctype, const obj_pair& colliding) { - size_t min_queue_length = std::numeric_limits::max(); - size_t target_thread = 0; + // Wake every worker parked in its idle wait. Taking the queue mutex before notifying is + // what makes this race free: a worker only ever parks while holding that mutex, so we + // cannot slip the notify into the gap between its "am I done?" check and the wait itself. for (size_t i = 0; i < threading::get_num_workers(); i++) { - size_t queue_length = collision_thread_data_buffer[i].queue_length.load(std::memory_order_acquire); - if (queue_length == 0) { - target_thread = i; - break; - } - else if (queue_length < min_queue_length) { - target_thread = i; - min_queue_length = queue_length; + auto& thread = collision_thread_data_buffer[i]; + { + std::scoped_lock lock(thread.queue_mutex); } + thread.work_available.notify_all(); } - { - auto& thread = collision_thread_data_buffer[target_thread]; - std::scoped_lock lock(thread.queue_mutex); - thread.queue_load->emplace_back( collision_thread_data::collision_queue_item{colliding, ctype} ); - thread.queue_length.fetch_add(1, std::memory_order_release); + + threading::spin_down_wait_complete(); +} + +// Pairs are staged on the main thread and handed to the workers in chunks. Locking a worker +// queue for every individual pair cost ~0.8us per pair -- about as much as the narrowphase work +// being offloaded -- because the producer contends with seven workers that are constantly +// parking and waking on that same mutex. +SCP_vector collision_stage; +constexpr size_t collision_stage_flush_size = 256; + +void flush_mp_collisions() { + if (collision_stage.empty()) + return; + + const size_t workers = threading::get_num_workers(); + size_t offset = 0; + + for (size_t i = 0; i < workers; i++) { + // deal the staged pairs out as evenly as the remainder allows + const size_t remaining = collision_stage.size() - offset; + const size_t take = remaining / (workers - i); + if (take == 0) + continue; + + auto& thread = collision_thread_data_buffer[i]; + { + std::scoped_lock lock(thread.queue_mutex); + thread.queue_load->insert(thread.queue_load->end(), + std::make_move_iterator(collision_stage.begin() + offset), + std::make_move_iterator(collision_stage.begin() + offset + take)); + thread.queue_length.fetch_add(take, std::memory_order_release); + } + thread.work_available.notify_one(); + + offset += take; } + + collision_stage.clear(); +} + +void queue_mp_collision(uint ctype, const obj_pair& colliding) { + collision_stage.emplace_back(collision_thread_data::collision_queue_item{colliding, ctype}); + + if (collision_stage.size() >= collision_stage_flush_size) + flush_mp_collisions(); } void post_process_threaded_collisions() { + collision_pending_results.clear(); + SCP_map workerThreads; for (size_t i = 0; i < threading::get_num_workers(); i++) workerThreads.emplace(i, 0); while (!workerThreads.empty()) { + COLLISION_PROF_INC(drain_spins); + + bool progress = false; + for(auto& [i, processed] : workerThreads) { auto& thread = collision_thread_data_buffer[i]; @@ -801,35 +989,63 @@ void post_process_threaded_collisions() { size_t result_length = thread.result_length.load(std::memory_order_acquire); if (result_length > processed) { + progress = true; { std::scoped_lock lock(thread.result_mutex); thread.queue_results.swap(thread.queue_send); } + processed += thread.queue_send->size(); + // Only collect here. Applying a collision mutates ship physics and hull state, and + // the other workers are still running checks that read those same objects, so the + // actual processing waits until after spin down. for (auto& collision : *thread.queue_send) { - uint key = (OBJ_INDEX(collision.objs.a) << collision_cache_bitshift) + OBJ_INDEX(collision.objs.b); - collider_pair *collision_info = &Collision_cached_pairs[key]; - - if (collision.collision_data.has_value()) - collision.process_collision(&collision.objs, collision.collision_data); - - if (collision.never_recheck) { - collision_info->next_check_time = -1; - } else { - collision_info->next_check_time = collision.objs.next_check_time; - } + collision_pending_results.emplace_back(std::move(collision)); } - processed += thread.queue_send->size(); thread.queue_send->clear(); } else if (queue_length == 0) { thread.queue_results->clear(); workerThreads.erase(i); + progress = true; break; } } + + if (!progress) { + // Every worker still has outstanding work but none of it has landed yet. Yield + // rather than sleeping on a condition variable: the main thread has nothing else to + // do, results land in tens of microseconds, and a timed wait rounds every one of + // those up to its own granularity (a 50us wait_for here cost ~2.3ms/frame). This + // loop was only ~700 iterations a frame even before the worker spin was fixed, so + // it was never the thing burning cores. + std::this_thread::yield(); + } } spin_down_mp_collision(); + + // Every worker has now stopped, so it is safe to mutate the objects the checks were reading. + for (auto& collision : collision_pending_results) { + if (collision.collision_data.has_value()) + collision.process_collision(&collision.objs, collision.collision_data); + + // Look the entry up only after process_collision has run -- it executes game logic that + // can compact the pair cache out from under us. + uint key = (OBJ_INDEX(collision.objs.a) << collision_cache_bitshift) + OBJ_INDEX(collision.objs.b); + bool existed = false; + collider_pair *collision_info = Collision_cached_pairs.get_or_create(key, existed); + if (!existed) { + collision_info->signature_a = collision.objs.a->signature; + collision_info->signature_b = collision.objs.b->signature; + } + + if (collision.never_recheck) { + collision_info->next_check_time = -1; + } else { + collision_info->next_check_time = collision.objs.next_check_time; + } + } + collision_pending_results.clear(); } void obj_collide_pair(object *A, object *B) @@ -840,6 +1056,8 @@ void obj_collide_pair(object *A, object *B) int swapped = 0; bool support_mp = false; + COLLISION_PROF_INC(pair_calls); + if ( A==B ) return; // Don't check collisions with yourself if ( !(A->flags[Object::Object_Flags::Collides]) ) return; // This object doesn't collide with anything @@ -871,17 +1089,21 @@ void obj_collide_pair(object *A, object *B) break; case COLLISION_OF(OBJ_DEBRIS, OBJ_WEAPON): check_collision = collide_debris_weapon; + support_mp = true; break; case COLLISION_OF(OBJ_WEAPON, OBJ_DEBRIS): swapped = 1; check_collision = collide_debris_weapon; + support_mp = true; break; case COLLISION_OF(OBJ_DEBRIS, OBJ_SHIP): check_collision = collide_debris_ship; + support_mp = true; break; case COLLISION_OF(OBJ_SHIP, OBJ_DEBRIS): check_collision = collide_debris_ship; swapped = 1; + support_mp = true; break; case COLLISION_OF(OBJ_DEBRIS, OBJ_PROP): check_collision = collide_debris_prop; @@ -892,17 +1114,21 @@ void obj_collide_pair(object *A, object *B) break; case COLLISION_OF(OBJ_ASTEROID, OBJ_WEAPON): check_collision = collide_asteroid_weapon; + support_mp = true; break; case COLLISION_OF(OBJ_WEAPON, OBJ_ASTEROID): swapped = 1; check_collision = collide_asteroid_weapon; + support_mp = true; break; case COLLISION_OF(OBJ_ASTEROID, OBJ_SHIP): check_collision = collide_asteroid_ship; + support_mp = true; break; case COLLISION_OF(OBJ_SHIP, OBJ_ASTEROID): check_collision = collide_asteroid_ship; swapped = 1; + support_mp = true; break; case COLLISION_OF(OBJ_ASTEROID, OBJ_PROP): check_collision = collide_asteroid_prop; @@ -1014,6 +1240,7 @@ void obj_collide_pair(object *A, object *B) } else { check_collision = collide_weapon_weapon; } + support_mp = true; } break; @@ -1025,6 +1252,8 @@ void obj_collide_pair(object *A, object *B) if ( !check_collision ) return; + COLLISION_PROF_INC(pairs_considered); + // Swap them if needed if ( swapped ) { std::swap(A,B); @@ -1033,34 +1262,29 @@ void obj_collide_pair(object *A, object *B) bool valid = false; uint key = (OBJ_INDEX(A) << collision_cache_bitshift) + OBJ_INDEX(B); - collider_pair* collision_info = &Collision_cached_pairs[key]; + // NOTE: timing this single lookup costs ~1.5ms/frame in timer calls alone, which is more + // than it measures. It was instrumented once to establish that the lookup was ~2.3ms of a + // 5.3ms collision budget; don't leave a timer here. + bool existed = false; + collider_pair* collision_info = Collision_cached_pairs.get_or_create(key, existed); - if ( collision_info->initialized ) { - // make sure we're referring to the correct objects in case the original pair was deleted - if ( collision_info->signature_a == collision_info->a->signature && - collision_info->signature_b == collision_info->b->signature ) { - valid = true; - } else { - collision_info->a = A; - collision_info->b = B; - collision_info->signature_a = A->signature; - collision_info->signature_b = B->signature; - collision_info->next_check_time = timestamp(0); - } + // The key is built from both objnums, so the entry always describes these two slots. All we + // have to confirm is that neither slot has been recycled since the entry was made. + if ( existed && collision_info->signature_a == A->signature && collision_info->signature_b == B->signature ) { + valid = true; } else { - collision_info->a = A; - collision_info->b = B; collision_info->signature_a = A->signature; collision_info->signature_b = B->signature; - collision_info->initialized = true; collision_info->next_check_time = timestamp(0); } if ( valid ) { // if this signature is valid, make the necessary checks to see if we need to collide check if ( collision_info->next_check_time == -1 ) { + COLLISION_PROF_INC(pairs_cache_skipped); return; } else if ( !timestamp_elapsed(collision_info->next_check_time) ) { + COLLISION_PROF_INC(pairs_cache_skipped); return; } } else { @@ -1130,19 +1354,63 @@ void obj_collide_pair(object *A, object *B) new_pair.next_check_time = collision_info->next_check_time; if (threading::is_threading() && support_mp) { + COLLISION_PROF_INC(pairs_enqueued); queue_mp_collision(ctype, new_pair); } else { - if (check_collision(&new_pair)) { + COLLISION_PROF_INC(pairs_checked_inline); +#if COLLISION_PROFILING + switch (A->type == OBJ_BEAM || B->type == OBJ_BEAM ? OBJ_BEAM : ctype) { + case OBJ_BEAM: + COLLISION_PROF_INC(inline_beam); break; + case COLLISION_OF(OBJ_WEAPON, OBJ_WEAPON): + COLLISION_PROF_INC(inline_weapon_weapon); break; + case COLLISION_OF(OBJ_DEBRIS, OBJ_SHIP): + case COLLISION_OF(OBJ_SHIP, OBJ_DEBRIS): + COLLISION_PROF_INC(inline_debris_ship); break; + case COLLISION_OF(OBJ_ASTEROID, OBJ_SHIP): + case COLLISION_OF(OBJ_SHIP, OBJ_ASTEROID): + COLLISION_PROF_INC(inline_asteroid_ship); break; + case COLLISION_OF(OBJ_PROP, OBJ_SHIP): + case COLLISION_OF(OBJ_SHIP, OBJ_PROP): + case COLLISION_OF(OBJ_PROP, OBJ_WEAPON): + case COLLISION_OF(OBJ_WEAPON, OBJ_PROP): + case COLLISION_OF(OBJ_DEBRIS, OBJ_PROP): + case COLLISION_OF(OBJ_PROP, OBJ_DEBRIS): + case COLLISION_OF(OBJ_ASTEROID, OBJ_PROP): + case COLLISION_OF(OBJ_PROP, OBJ_ASTEROID): + COLLISION_PROF_INC(inline_prop); break; + default: + COLLISION_PROF_INC(inline_other); break; + } +#endif +#if COLLISION_PROFILING + const std::uint64_t narrow_start_ns = timer_get_nanoseconds(); +#endif + const int hit = check_collision(&new_pair); + COLLISION_PROF_ADD(narrowphase_inline_ns, timer_get_nanoseconds() - narrow_start_ns); + + // Re-acquire rather than reusing collision_info: check_collision runs arbitrary game + // logic, and creating an object can reach collide_remove_weapons(), which compacts the + // pair cache and invalidates every pointer into it. (The old SCP_unordered_map had + // stable node addresses and did not need this.) + bool still_present = false; + collider_pair *updated = Collision_cached_pairs.get_or_create(key, still_present); + if (!still_present) { + updated->signature_a = A->signature; + updated->signature_b = B->signature; + } + + if (hit) { // don't have to check ever again - collision_info->next_check_time = -1; + updated->next_check_time = -1; } else { - collision_info->next_check_time = new_pair.next_check_time; + updated->next_check_time = new_pair.next_check_time; } } } -void obj_find_overlap_colliders(SCP_vector &overlap_list_out, SCP_vector &list, int axis, bool collide) +void obj_find_overlap_colliders(SCP_vector &overlap_list_out, SCP_vector &list, bool collide) { TRACE_SCOPE(tracing::FindOverlapColliders); @@ -1152,10 +1420,10 @@ void obj_find_overlap_colliders(SCP_vector &overlap_list_out, SCP_vectorclear(); } + else { + // Nothing to do, and we cannot leave until the main thread says collision is done, + // so park instead of spinning. Re-checking both conditions under the queue mutex is + // what closes the lost-wakeup window: queue_mp_collision bumps queue_length while + // holding this mutex, and spin_down_mp_collision takes it before notifying. + COLLISION_PROF_INC(worker_idle_spins); + + std::unique_lock lock(thread.queue_mutex); + if (thread.queue_length.load(std::memory_order_acquire) == 0 + && !collision_processing_done.load(std::memory_order_acquire)) { + thread.work_available.wait(lock); + } + } } + + collision_profiling::flush_local(); } void collide_init() { @@ -1240,6 +1542,7 @@ void collide_init() { } // used only in obj_sort_and_collide() +static SCP_vector active_collision_list; static SCP_vector sort_list_y; static SCP_vector sort_list_z; @@ -1251,8 +1554,7 @@ void obj_sort_and_collide(SCP_vector* Collision_list) if ( !(Game_detail_flags & DETAIL_FLAG_COLLISION) ) return; - if (threading::is_threading()) - spin_up_mp_collision(); + const std::uint64_t collide_start_ns = timer_get_nanoseconds(); if (!Collision_cache_stale_objects.empty()) { obj_collide_retime_stale_pairs(); @@ -1264,29 +1566,78 @@ void obj_sort_and_collide(SCP_vector* Collision_list) Collision_list = &Collision_sort_list; } + // obj_add_collider() only tests Not_in_coll, so objects that cannot collide are still in the + // list. They get sorted, they get swept, and they generate pairs that obj_collide_pair() + // then rejects on flags. Drop them once here instead. Filtering per frame rather than at + // insertion time means an object whose Collides flag changes during the mission is handled + // with no extra bookkeeping. + active_collision_list.clear(); + active_collision_list.reserve(Collision_list->size()); + for (int objnum : *Collision_list) { + if (Objects[objnum].flags[Object::Object_Flags::Collides]) + active_collision_list.push_back(objnum); + } + Collision_list = &active_collision_list; + + std::uint64_t phase_ns = timer_get_nanoseconds(); + sort_list_y.clear(); { TRACE_SCOPE(tracing::SortColliders); + obj_cache_collider_endpoints(*Collision_list, 0); obj_quicksort_colliders(Collision_list, 0, (int)(Collision_list->size() - 1), 0); } - obj_find_overlap_colliders(sort_list_y, *Collision_list, 0, false); + COLLISION_PROF_FRAME_ADD(sort_ns, timer_get_nanoseconds() - phase_ns); + phase_ns = timer_get_nanoseconds(); + + obj_find_overlap_colliders(sort_list_y, *Collision_list, false); + + COLLISION_PROF_FRAME_ADD(overlap_ns, timer_get_nanoseconds() - phase_ns); + phase_ns = timer_get_nanoseconds(); sort_list_z.clear(); { TRACE_SCOPE(tracing::SortColliders); + obj_cache_collider_endpoints(sort_list_y, 1); obj_quicksort_colliders(&sort_list_y, 0, (int)(sort_list_y.size() - 1), 1); } - obj_find_overlap_colliders(sort_list_z, sort_list_y, 1, false); + COLLISION_PROF_FRAME_ADD(sort_ns, timer_get_nanoseconds() - phase_ns); + phase_ns = timer_get_nanoseconds(); + + obj_find_overlap_colliders(sort_list_z, sort_list_y, false); + + COLLISION_PROF_FRAME_ADD(overlap_ns, timer_get_nanoseconds() - phase_ns); + phase_ns = timer_get_nanoseconds(); sort_list_y.clear(); { TRACE_SCOPE(tracing::SortColliders); + obj_cache_collider_endpoints(sort_list_z, 2); obj_quicksort_colliders(&sort_list_z, 0, (int)(sort_list_z.size() - 1), 2); } - obj_find_overlap_colliders(sort_list_y, sort_list_z, 2, true); + COLLISION_PROF_FRAME_ADD(sort_ns, timer_get_nanoseconds() - phase_ns); + phase_ns = timer_get_nanoseconds(); + // Only this last pass generates pairs, so the workers have nothing to do before it. Spinning + // them up any earlier just parks every one of them in an idle wait for the whole broadphase. if (threading::is_threading()) + spin_up_mp_collision(); + + obj_find_overlap_colliders(sort_list_y, sort_list_z, true); + + COLLISION_PROF_FRAME_ADD(overlap_ns, timer_get_nanoseconds() - phase_ns); + phase_ns = timer_get_nanoseconds(); + + if (threading::is_threading()) { + flush_mp_collisions(); post_process_threaded_collisions(); + } + + COLLISION_PROF_FRAME_ADD(drain_ns, timer_get_nanoseconds() - phase_ns); + + COLLISION_PROF_FRAME_ADD(collision_ns, timer_get_nanoseconds() - collide_start_ns); + COLLISION_PROF_FRAME_ADD(cache_size, Collision_cached_pairs.size()); + collision_profiling::flush_local(); } void collide_apply_gravity_flags_weapons() { diff --git a/code/object/objcollide.h b/code/object/objcollide.h index ac85382a9ac..122c3dd3550 100644 --- a/code/object/objcollide.h +++ b/code/object/objcollide.h @@ -101,6 +101,7 @@ void collide_init(); // Returns 1 if all future collisions between these can be ignored // CODE is locatated in CollideWeaponWeapon.cpp int collide_weapon_weapon( obj_pair * pair ); +collision_result collide_weapon_weapon_check( obj_pair * pair ); // Checks ship-weapon collisions. pair->a is ship and pair->b is weapon. // Returns 1 if all future collisions between these can be ignored @@ -114,11 +115,13 @@ collision_result collide_ship_weapon_check( obj_pair * pair ); // Returns 1 if all future collisions between these can be ignored // CODE is locatated in CollideDebrisWeapon.cpp int collide_debris_weapon( obj_pair * pair ); +collision_result collide_debris_weapon_check( obj_pair * pair ); // Checks debris-ship collisions. pair->a is debris and pair->b is ship. // Returns 1 if all future collisions between these can be ignored // CODE is locatated in CollideDebrisShip.cpp int collide_debris_ship( obj_pair * pair ); +collision_result collide_debris_ship_check( obj_pair * pair ); // Checks debris-prop collisions. pair->a is debris and pair->b is prop. // Returns 1 if all future collisions between these can be ignored @@ -127,7 +130,9 @@ int collide_debris_prop(obj_pair* pair); int collide_asteroid_prop(obj_pair* pair); int collide_asteroid_ship(obj_pair *pair); +collision_result collide_asteroid_ship_check( obj_pair * pair ); int collide_asteroid_weapon(obj_pair *pair); +collision_result collide_asteroid_weapon_check( obj_pair * pair ); // Checks ship-ship collisions. pair->a and pair->b are ships. // Returns 1 if all future collisions between these can be ignored diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 913aeff5543..02b973386c3 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -1105,6 +1105,8 @@ add_file_folder("Object" object/collideshipship.cpp object/collideshipweapon.cpp object/collideweaponweapon.cpp + object/collideprofile.cpp + object/collideprofile.h object/deadobjectdock.cpp object/deadobjectdock.h object/objcollide.cpp diff --git a/code/weapon/beam.cpp b/code/weapon/beam.cpp index 0466130d365..e53d0f820ff 100644 --- a/code/weapon/beam.cpp +++ b/code/weapon/beam.cpp @@ -143,6 +143,7 @@ void beam_get_binfo(beam* b, float accuracy, int num_shots, int burst_seed, floa // aim the beam (setup last_start and last_shot - the endpoints). also recalculates object collision info void beam_aim(beam *b); +static void beam_update_cull_info(beam *b); // direct fire type functions void beam_type_direct_fire_move(beam *b); @@ -1156,10 +1157,33 @@ void beam_type_omni_move(beam* b) } // pre-move (before collision checking - but AFTER ALL OTHER OBJECTS HAVE BEEN MOVED) +// Refresh the per-beam values that the broadphase cull uses. Called once per beam per frame, +// after the beam has been aimed, so that beam_collide_early_out() only has to read them. +static void beam_update_cull_info(beam *b) +{ + vec3d rel; + vm_vec_sub(&rel, &b->last_shot, &b->last_start); + + b->cull_len = vm_vec_mag(&rel); + b->cull_radius = b->beam_collide_width * b->current_width_factor * 0.5f; + + // same threshold fvi_cylinder_sphere_may_collide() uses, which this cull replaces + const float cull_small_num = 1E-6f; + + if (b->cull_len < cull_small_num) { + b->cull_dir = vmd_zero_vector; + b->cull_valid = false; + return; + } + + vm_vec_copy_scale(&b->cull_dir, &rel, 1.0f / b->cull_len); + b->cull_valid = true; +} + void beam_move_all_pre() -{ - beam *b; - beam *moveup; +{ + beam *b; + beam *moveup; // zero lights for this frame yet Beam_light_count = 0; @@ -1234,6 +1258,10 @@ void beam_move_all_pre() } } + // Cache the values that beam_collide_early_out() needs. They are constant for this beam + // for the whole collision phase, and that function runs once per candidate pair. + beam_update_cull_info(b); + // next moveup = GET_NEXT(moveup); } @@ -2971,6 +2999,10 @@ void beam_aim(beam *b) UNREACHABLE("Impossible beam type (%d); get a coder!\n", static_cast(b->type)); } + // Keep the broadphase cull values in step with the new aim. beam_move_all_pre() also does + // this every frame, but a beam can be fired after that point, for example from a SEXP. + beam_update_cull_info(b); + if (!Weapon_info[b->weapon_info_index].wi_flags[Weapon::Info_Flags::No_collide]) { // recalculate object pairs OBJ_RECALC_PAIRS((&Objects[b->objnum])); @@ -3867,9 +3899,37 @@ int beam_collide_early_out(object *a, object *b) return 1; } - if((vm_vec_dist(&bm->last_start, &b->pos)-b->radius) > bwi->b_info.range){ - return 1; - }//if the object is too far away, don't bother trying to colide with it-Bobboau + // Reject on the beam segment before anything else. This used to be a distance test against + // last_start only, which ignores the beam direction entirely and therefore kept every object + // within range in any direction. The segment test below is what actually decides the pair, + // so doing it first lets the great majority of candidates leave here immediately. + // + // The values come from beam_update_cull_info(), which runs once per beam per frame. + { + const float sphere_rad = b->radius * 1.2f; + const float sum_rad = bm->cull_radius + sphere_rad; + + vec3d sphere_rel; + vm_vec_sub(&sphere_rel, &b->pos, &bm->last_start); + + if (!bm->cull_valid) { + // zero length segment: fall back to a sphere test around the start point + if (vm_vec_mag_squared(&sphere_rel) > sum_rad * sum_rad) { + return 1; + } + } else { + const float axial = vm_vec_dot(&bm->cull_dir, &sphere_rel); + if (axial < -sphere_rad || axial > bm->cull_len + sphere_rad) { + return 1; + } + + vec3d radial; + vm_vec_scale_add(&radial, &sphere_rel, &bm->cull_dir, -axial); + if (vm_vec_mag_squared(&radial) > sum_rad * sum_rad) { + return 1; + } + } + } // baseline bails switch(b->type){ @@ -3915,13 +3975,8 @@ int beam_collide_early_out(object *a, object *b) break; } - float beam_radius = bm->beam_collide_width * bm->current_width_factor * 0.5f; - // do a cylinder-sphere collision test - if (!fvi_cylinder_sphere_may_collide(&bm->last_start, &bm->last_shot, - beam_radius, &b->pos, b->radius * 1.2f)) { - return 1; - } - + // the segment test at the top of this function has already been done + // don't cull return 0; } diff --git a/code/weapon/beam.h b/code/weapon/beam.h index c34181eaa0f..18d6659daa9 100644 --- a/code/weapon/beam.h +++ b/code/weapon/beam.h @@ -210,6 +210,15 @@ typedef struct beam { int bank; int firingpoint; + + // Broadphase culling values, refreshed once per frame by beam_move_all_pre() after the beam + // has been aimed. beam_collide_early_out() runs tens of thousands of times per frame, and + // deriving these per call cost a sqrt and a divide every time. + vec3d cull_dir; // normalized last_start -> last_shot + float cull_len; // length of that segment + float cull_radius; // half the collide width, scaled by the current width factor + bool cull_valid; // false while warming up or down, when last_shot is not aimed yet + float beam_collide_width; float beam_light_width; diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index c4e1e53c5dc..6a1a3727b02 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -146,6 +146,7 @@ #include "network/multiui.h" #include "network/multiutil.h" #include "network/stand_gui.h" +#include "object/collideprofile.h" #include "object/objcollide.h" #include "object/objectsnd.h" #include "object/waypoint.h" @@ -4677,6 +4678,8 @@ void game_do_frame(bool set_frametime) last_single_step = game_single_step; game_frame(); + + collision_profiling::benchmark_frame(); } void multi_maybe_do_frame()