Simplify sort/max_by calls (#17048)

# Objective

Some sort calls and `Ord` impls are unnecessarily complex.

## Solution

Rewrite the "match on cmp, if equal do another cmp" as either a
comparison on tuples, or `Ordering::then_with`, depending on whether the
compare keys need construction.

`sort_by` -> `sort_by_key` when symmetrical. Do the same for
`min_by`/`max_by`.

Note that `total_cmp` can only work with `sort_by`, and not on tuples.

When sorting collected query results that contain
`Entity`/`MainEntity`/`RenderEntity` in their `QueryData`, with that
`Entity` in the sort key:
stable -> unstable sort (all queried entities are unique)

If key construction is not simple, switch to `sort_by_cached_key` when
possible.

Sorts that are only performed to discover the maximal element are
replaced by `max_by_key`.

Dedicated comparison functions and structs are removed where simple.

Derive `PartialOrd`/`Ord` when useful.

Misc. closure style inconsistencies.

## Testing
- Existing tests.
This commit is contained in:
Vic 2024-12-30 23:59:36 +01:00 committed by GitHub
parent 1e9f647b33
commit b78efd339d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 41 additions and 159 deletions

View File

@ -125,7 +125,7 @@ impl Animatable for bool {
#[inline]
fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self {
inputs
.max_by(|a, b| FloatOrd(a.weight).cmp(&FloatOrd(b.weight)))
.max_by_key(|x| FloatOrd(x.weight))
.map(|input| input.value)
.unwrap_or(false)
}

View File

@ -3,7 +3,7 @@ use core::fmt::Debug;
/// Unique identifier for a system or system set stored in a [`ScheduleGraph`].
///
/// [`ScheduleGraph`]: crate::schedule::ScheduleGraph
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum NodeId {
/// Identifier for a system.
System(usize),
@ -11,18 +11,6 @@ pub enum NodeId {
Set(usize),
}
impl PartialOrd for NodeId {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(Ord::cmp(self, other))
}
}
impl Ord for NodeId {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.cmp(other)
}
}
impl NodeId {
/// Returns the internal integer value.
pub(crate) const fn index(&self) -> usize {

View File

@ -45,7 +45,7 @@ use crate::{FocusedInput, InputFocus, InputFocusVisible};
///
/// Note that you must also add the [`TabGroup`] component to the entity's ancestor in order
/// for this component to have any effect.
#[derive(Debug, Default, Component, Copy, Clone)]
#[derive(Debug, Default, Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct TabIndex(pub i32);
/// A component used to mark a tree of entities as containing tabbable elements.
@ -179,7 +179,7 @@ impl TabNavigation<'_, '_> {
.map(|(e, tg, _)| (e, *tg))
.collect();
// Stable sort by group order
tab_groups.sort_by(compare_tab_groups);
tab_groups.sort_by_key(|(_, tg)| tg.order);
// Search group descendants
tab_groups.iter().for_each(|(tg_entity, _)| {
@ -194,7 +194,7 @@ impl TabNavigation<'_, '_> {
}
// Stable sort by tabindex
focusable.sort_by(compare_tab_indices);
focusable.sort_by_key(|(_, idx)| *idx);
let index = focusable.iter().position(|e| Some(e.0) == focus.0);
let count = focusable.len();
@ -233,15 +233,6 @@ impl TabNavigation<'_, '_> {
}
}
fn compare_tab_groups(a: &(Entity, TabGroup), b: &(Entity, TabGroup)) -> core::cmp::Ordering {
a.1.order.cmp(&b.1.order)
}
// Stable sort which compares by tab index
fn compare_tab_indices(a: &(Entity, TabIndex), b: &(Entity, TabIndex)) -> core::cmp::Ordering {
a.1 .0.cmp(&b.1 .0)
}
/// Plugin for navigating between focusable entities using keyboard input.
pub struct TabNavigationPlugin;

View File

@ -52,10 +52,7 @@ impl Ord for SweepLineEvent {
/// Orders 2D points according to the order expected by the sweep line and event queue from -X to +X and then -Y to Y.
fn xy_order(a: Vec2, b: Vec2) -> Ordering {
match a.x.total_cmp(&b.x) {
Ordering::Equal => a.y.total_cmp(&b.y),
ord => ord,
}
a.x.total_cmp(&b.x).then_with(|| a.y.total_cmp(&b.y))
}
/// The event queue holds an ordered list of all events the [`SweepLine`] will encounter when checking the current polygon.
@ -121,6 +118,7 @@ impl PartialEq for Segment {
}
}
impl Eq for Segment {}
impl PartialOrd for Segment {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
@ -128,10 +126,10 @@ impl PartialOrd for Segment {
}
impl Ord for Segment {
fn cmp(&self, other: &Self) -> Ordering {
match self.left.y.total_cmp(&other.left.y) {
Ordering::Equal => self.right.y.total_cmp(&other.right.y),
ord => ord,
}
self.left
.y
.total_cmp(&other.left.y)
.then_with(|| self.right.y.total_cmp(&other.right.y))
}
}

View File

@ -26,8 +26,6 @@ use crate::{
MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS,
};
use super::ClusterableObjectOrderData;
const NDC_MIN: Vec2 = Vec2::NEG_ONE;
const NDC_MAX: Vec2 = Vec2::ONE;
@ -254,16 +252,10 @@ pub(crate) fn assign_objects_to_clusters(
if clusterable_objects.len() > MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS
&& !supports_storage_buffers
{
clusterable_objects.sort_by(|clusterable_object_1, clusterable_object_2| {
crate::clusterable_object_order(
ClusterableObjectOrderData {
entity: &clusterable_object_1.entity,
object_type: &clusterable_object_1.object_type,
},
ClusterableObjectOrderData {
entity: &clusterable_object_2.entity,
object_type: &clusterable_object_2.object_type,
},
clusterable_objects.sort_by_cached_key(|clusterable_object| {
(
clusterable_object.object_type.ordering(),
clusterable_object.entity,
)
});

View File

@ -2,7 +2,6 @@
use core::num::NonZero;
use self::assign::ClusterableObjectType;
use bevy_core_pipeline::core_3d::Camera3d;
use bevy_ecs::{
component::Component,
@ -516,34 +515,6 @@ impl Default for GpuClusterableObjectsUniform {
}
}
pub(crate) struct ClusterableObjectOrderData<'a> {
pub(crate) entity: &'a Entity,
pub(crate) object_type: &'a ClusterableObjectType,
}
#[allow(clippy::too_many_arguments)]
// Sort clusterable objects by:
//
// * object type, so that we can iterate point lights, spot lights, etc. in
// contiguous blocks in the fragment shader,
//
// * then those with shadows enabled first, so that the index can be used to
// render at most `point_light_shadow_maps_count` point light shadows and
// `spot_light_shadow_maps_count` spot light shadow maps,
//
// * then by entity as a stable key to ensure that a consistent set of
// clusterable objects are chosen if the clusterable object count limit is
// exceeded.
pub(crate) fn clusterable_object_order(
a: ClusterableObjectOrderData,
b: ClusterableObjectOrderData,
) -> core::cmp::Ordering {
a.object_type
.ordering()
.cmp(&b.object_type.ordering())
.then_with(|| a.entity.cmp(b.entity)) // stable
}
/// Extracts clusters from the main world from the render world.
pub fn extract_clusters(
mut commands: Commands,

View File

@ -522,24 +522,6 @@ pub enum SimulationLightSystems {
CheckLightVisibility,
}
// Sort lights by
// - those with volumetric (and shadows) enabled first, so that the volumetric
// lighting pass can quickly find the volumetric lights;
// - then those with shadows enabled second, so that the index can be used to
// render at most `directional_light_shadow_maps_count` directional light
// shadows;
// - then by entity as a stable key to ensure that a consistent set of lights
// are chosen if the light count limit is exceeded.
pub(crate) fn directional_light_order(
(entity_1, volumetric_1, shadows_enabled_1): (&Entity, &bool, &bool),
(entity_2, volumetric_2, shadows_enabled_2): (&Entity, &bool, &bool),
) -> core::cmp::Ordering {
volumetric_2
.cmp(volumetric_1) // volumetric before shadows
.then_with(|| shadows_enabled_2.cmp(shadows_enabled_1)) // shadow casters before non-casters
.then_with(|| entity_1.cmp(entity_2)) // stable
}
pub fn update_directional_light_frusta(
mut views: Query<
(

View File

@ -837,16 +837,10 @@ pub fn prepare_lights(
// - then those with shadows enabled first, so that the index can be used to render at most `point_light_shadow_maps_count`
// point light shadows and `spot_light_shadow_maps_count` spot light shadow maps,
// - then by entity as a stable key to ensure that a consistent set of lights are chosen if the light count limit is exceeded.
point_lights.sort_by(|(entity_1, light_1, _), (entity_2, light_2, _)| {
clusterable_object_order(
ClusterableObjectOrderData {
entity: entity_1,
object_type: &ClusterableObjectType::from_point_or_spot_light(light_1),
},
ClusterableObjectOrderData {
entity: entity_2,
object_type: &ClusterableObjectType::from_point_or_spot_light(light_2),
},
point_lights.sort_by_cached_key(|(entity, light, _)| {
(
ClusterableObjectType::from_point_or_spot_light(light).ordering(),
*entity,
)
});
@ -858,12 +852,10 @@ pub fn prepare_lights(
// shadows
// - then by entity as a stable key to ensure that a consistent set of
// lights are chosen if the light count limit is exceeded.
directional_lights.sort_by(|(entity_1, light_1), (entity_2, light_2)| {
directional_light_order(
(entity_1, &light_1.volumetric, &light_1.shadows_enabled),
(entity_2, &light_2.volumetric, &light_2.shadows_enabled),
)
});
// - because entities are unique, we can use `sort_unstable_by_key`
// and still end up with a stable order.
directional_lights
.sort_unstable_by_key(|(entity, light)| (light.volumetric, light.shadows_enabled, *entity));
if global_light_meta.entity_to_index.capacity() < point_lights.len() {
global_light_meta

View File

@ -231,7 +231,7 @@ pub fn update_interactions(
if let Some(pointers_hovered_entities) = hover_map.get(pointer) {
// Insert a sorted list of hit entities into the pointer's interaction component.
let mut sorted_entities: Vec<_> = pointers_hovered_entities.clone().drain().collect();
sorted_entities.sort_by_key(|(_entity, hit)| FloatOrd(hit.depth));
sorted_entities.sort_by_key(|(_, hit)| FloatOrd(hit.depth));
pointer_interaction.sorted_entities = sorted_entities;
for hovered_entity in pointers_hovered_entities.iter().map(|(entity, _)| entity) {

View File

@ -1220,10 +1220,7 @@ pub fn sort_cameras(
// sort by order and ensure within an order, RenderTargets of the same type are packed together
sorted_cameras
.0
.sort_by(|c1, c2| match c1.order.cmp(&c2.order) {
core::cmp::Ordering::Equal => c1.target.cmp(&c2.target),
ord => ord,
});
.sort_by(|c1, c2| (c1.order, &c1.target).cmp(&(c2.order, &c2.target)));
let mut previous_order_target = None;
let mut ambiguities = <HashSet<_>>::default();
let mut target_counts = <HashMap<_, _>>::default();

View File

@ -180,7 +180,7 @@ impl<'a> Serialize for SceneMapSerializer<'a> {
)
})
.collect::<Vec<_>>();
entries.sort_by_key(|(type_path, _partial_reflect)| *type_path);
entries.sort_by_key(|(type_path, _)| *type_path);
entries
};

View File

@ -333,52 +333,26 @@ impl WinitWindows {
///
/// The heuristic for "best" prioritizes width, height, and refresh rate in that order.
pub fn get_fitting_videomode(monitor: &MonitorHandle, width: u32, height: u32) -> VideoModeHandle {
let mut modes = monitor.video_modes().collect::<Vec<_>>();
fn abs_diff(a: u32, b: u32) -> u32 {
if a > b {
return a - b;
}
b - a
}
modes.sort_by(|a, b| {
use core::cmp::Ordering::*;
match abs_diff(a.size().width, width).cmp(&abs_diff(b.size().width, width)) {
Equal => {
match abs_diff(a.size().height, height).cmp(&abs_diff(b.size().height, height)) {
Equal => b
.refresh_rate_millihertz()
.cmp(&a.refresh_rate_millihertz()),
default => default,
}
}
default => default,
}
});
modes.first().unwrap().clone()
monitor
.video_modes()
.max_by_key(|x| {
(
x.size().width.abs_diff(width),
x.size().height.abs_diff(height),
x.refresh_rate_millihertz(),
)
})
.unwrap()
}
/// Gets the "best" video-mode handle from a monitor.
///
/// The heuristic for "best" prioritizes width, height, and refresh rate in that order.
pub fn get_best_videomode(monitor: &MonitorHandle) -> VideoModeHandle {
let mut modes = monitor.video_modes().collect::<Vec<_>>();
modes.sort_by(|a, b| {
use core::cmp::Ordering::*;
match b.size().width.cmp(&a.size().width) {
Equal => match b.size().height.cmp(&a.size().height) {
Equal => b
.refresh_rate_millihertz()
.cmp(&a.refresh_rate_millihertz()),
default => default,
},
default => default,
}
});
modes.first().unwrap().clone()
monitor
.video_modes()
.max_by_key(|x| (x.size(), x.refresh_rate_millihertz()))
.unwrap()
}
pub(crate) fn attempt_grab(

View File

@ -26,10 +26,7 @@ struct Example {
impl Ord for Example {
fn cmp(&self, other: &Self) -> Ordering {
match self.category.cmp(&other.category) {
Ordering::Equal => self.name.cmp(&other.name),
ordering => ordering,
}
(&self.category, &self.name).cmp(&(&other.category, &other.name))
}
}