use bevy::prelude::*;
use crate::plugin::MapStateResource;
#[derive(Resource)]
pub struct FrameChangeDetection {
prev_fingerprint: u64,
frame_changed: bool,
}
impl Default for FrameChangeDetection {
fn default() -> Self {
Self {
prev_fingerprint: u64::MAX,
frame_changed: true,
}
}
}
impl FrameChangeDetection {
pub fn changed(&self) -> bool {
self.frame_changed
}
}
pub fn update_frame_change_detection(
state: Res<MapStateResource>,
mut detection: ResMut<FrameChangeDetection>,
) {
let fp = compute_frame_fingerprint(&state.0);
detection.frame_changed = fp != detection.prev_fingerprint;
detection.prev_fingerprint = fp;
}
#[inline]
pub fn frame_unchanged(
detection: &FrameChangeDetection,
_current_state: &rustial_engine::MapState,
) -> bool {
!detection.frame_changed
}
fn compute_frame_fingerprint(state: &rustial_engine::MapState) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
let cam = state.camera();
let origin = state.scene_world_origin();
hash_i64(&mut h, (origin.x * 100.0) as i64);
hash_i64(&mut h, (origin.y * 100.0) as i64);
hash_i64(&mut h, (origin.z * 100.0) as i64);
hash_i64(&mut h, (cam.pitch() * 1000.0) as i64);
hash_i64(&mut h, (cam.yaw() * 1000.0) as i64);
hash_i64(&mut h, (cam.distance() * 10.0) as i64);
let proj_discriminant = match cam.projection() {
rustial_engine::CameraProjection::WebMercator => 0u64,
rustial_engine::CameraProjection::Equirectangular => 1,
rustial_engine::CameraProjection::Globe => 2,
rustial_engine::CameraProjection::VerticalPerspective { .. } => 3,
};
hash_u64(&mut h, proj_discriminant);
hash_u64(&mut h, cam.mode() as u64);
let tiles = state.visible_tiles();
hash_u64(&mut h, tiles.len() as u64);
if let Some(first) = tiles.first() {
hash_u64(&mut h, first.target.zoom as u64);
hash_u64(&mut h, first.target.x as u64);
hash_u64(&mut h, first.target.y as u64);
}
if let Some(last) = tiles.last() {
hash_u64(&mut h, last.target.zoom as u64);
hash_u64(&mut h, last.target.x as u64);
hash_u64(&mut h, last.target.y as u64);
}
let terrain = state.terrain_meshes();
hash_u64(&mut h, terrain.len() as u64);
for mesh in terrain {
hash_u64(&mut h, mesh.tile.zoom as u64);
hash_u64(&mut h, mesh.tile.x as u64);
hash_u64(&mut h, mesh.tile.y as u64);
hash_u64(&mut h, mesh.generation);
}
let vectors = state.vector_meshes();
hash_u64(&mut h, vectors.len() as u64);
for vmesh in vectors {
hash_u64(&mut h, vmesh.positions.len() as u64);
hash_u64(&mut h, vmesh.indices.len() as u64);
}
hash_u64(&mut h, state.model_instances().len() as u64);
hash_u64(&mut h, state.placed_symbols().len() as u64);
let frame = state.frame_output();
hash_u64(&mut h, frame.visualization.len() as u64);
for overlay in frame.visualization.iter() {
match overlay {
rustial_engine::VisualizationOverlay::GridScalar {
layer_id,
field,
ramp,
..
} => {
hash_u64(&mut h, 10);
hash_u64(&mut h, layer_id.as_u64());
hash_u64(&mut h, field.generation);
hash_u64(&mut h, field.value_generation);
hash_u64(&mut h, ramp.stops.len() as u64);
}
rustial_engine::VisualizationOverlay::GridExtrusion {
layer_id,
field,
params,
ramp,
..
} => {
hash_u64(&mut h, 11);
hash_u64(&mut h, layer_id.as_u64());
hash_u64(&mut h, field.generation);
hash_u64(&mut h, field.value_generation);
hash_u64(&mut h, params.height_scale.to_bits());
hash_u64(&mut h, params.base_meters.to_bits());
hash_u64(&mut h, ramp.stops.len() as u64);
}
rustial_engine::VisualizationOverlay::Columns {
layer_id,
columns,
ramp,
} => {
hash_u64(&mut h, 12);
hash_u64(&mut h, layer_id.as_u64());
hash_u64(&mut h, columns.generation);
hash_u64(&mut h, columns.columns.len() as u64);
hash_u64(&mut h, column_set_fingerprint(columns));
hash_u64(&mut h, ramp.stops.len() as u64);
}
rustial_engine::VisualizationOverlay::Points {
layer_id,
points,
ramp,
} => {
hash_u64(&mut h, 13);
hash_u64(&mut h, layer_id.as_u64());
hash_u64(&mut h, points.generation);
hash_u64(&mut h, points.points.len() as u64);
hash_u64(&mut h, point_set_fingerprint(points));
hash_u64(&mut h, ramp.stops.len() as u64);
}
}
}
let overlays = &frame.image_overlays;
hash_u64(&mut h, overlays.len() as u64);
for overlay in overlays.iter() {
hash_u64(&mut h, overlay.layer_id.as_u64());
hash_u64(&mut h, overlay.width as u64);
hash_u64(&mut h, overlay.height as u64);
hash_u64(&mut h, std::sync::Arc::as_ptr(&overlay.data) as u64);
}
if let Some(bg) = state.background_color() {
hash_u64(&mut h, bg[0].to_bits() as u64);
hash_u64(&mut h, bg[1].to_bits() as u64);
}
if let Some(hs) = state.hillshade() {
hash_u64(&mut h, hs.exaggeration.to_bits() as u64);
hash_u64(&mut h, hs.opacity.to_bits() as u64);
}
hash_u64(&mut h, state.terrain().enabled() as u64);
h
}
fn point_set_fingerprint(points: &rustial_engine::PointInstanceSet) -> u64 {
let mut h = points.points.len() as u64;
for point in &points.points {
hash_u64(&mut h, point.position.lat.to_bits());
hash_u64(&mut h, point.position.lon.to_bits());
hash_u64(&mut h, point.position.alt.to_bits());
hash_u64(&mut h, point.radius.to_bits());
hash_u64(&mut h, point.intensity.to_bits() as u64);
hash_u64(&mut h, point.pick_id);
hash_u64(
&mut h,
match point.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => 0,
rustial_engine::AltitudeMode::RelativeToGround => 1,
rustial_engine::AltitudeMode::Absolute => 2,
},
);
if let Some(color) = point.color {
hash_u64(&mut h, color[0].to_bits() as u64);
hash_u64(&mut h, color[1].to_bits() as u64);
hash_u64(&mut h, color[2].to_bits() as u64);
hash_u64(&mut h, color[3].to_bits() as u64);
}
}
h
}
#[inline(always)]
fn hash_u64(state: &mut u64, value: u64) {
*state ^= value;
*state = state.wrapping_mul(0x0100_0000_01b3);
}
#[inline(always)]
fn hash_i64(state: &mut u64, value: i64) {
hash_u64(state, value as u64);
}
fn column_set_fingerprint(columns: &rustial_engine::ColumnInstanceSet) -> u64 {
let mut h = columns.columns.len() as u64;
for column in &columns.columns {
hash_u64(&mut h, column.position.lat.to_bits());
hash_u64(&mut h, column.position.lon.to_bits());
hash_u64(&mut h, column.position.alt.to_bits());
hash_u64(&mut h, column.height.to_bits());
hash_u64(&mut h, column.base.to_bits());
hash_u64(&mut h, column.width.to_bits());
hash_u64(&mut h, column.pick_id);
hash_u64(
&mut h,
match column.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => 0,
rustial_engine::AltitudeMode::RelativeToGround => 1,
rustial_engine::AltitudeMode::Absolute => 2,
},
);
if let Some(color) = column.color {
hash_u64(&mut h, color[0].to_bits() as u64);
hash_u64(&mut h, color[1].to_bits() as u64);
hash_u64(&mut h, color[2].to_bits() as u64);
hash_u64(&mut h, color[3].to_bits() as u64);
}
}
h
}
#[cfg(test)]
mod tests {
use super::*;
use rustial_engine::{GeoCoord, MapState};
#[test]
fn identical_state_produces_identical_fingerprint() {
let state = MapState::new();
let fp1 = compute_frame_fingerprint(&state);
let fp2 = compute_frame_fingerprint(&state);
assert_eq!(fp1, fp2);
}
#[test]
fn moved_camera_produces_different_fingerprint() {
let mut state = MapState::new();
state.set_viewport(800, 600);
state.update();
let fp1 = compute_frame_fingerprint(&state);
state.set_camera_distance(5_000_000.0);
state.update();
let fp2 = compute_frame_fingerprint(&state);
assert_ne!(fp1, fp2);
}
#[test]
fn frame_unchanged_returns_true_for_steady_state() {
let state = MapState::new();
let mut detection = FrameChangeDetection::default();
assert!(detection.changed());
let fp = compute_frame_fingerprint(&state);
detection.frame_changed = fp != detection.prev_fingerprint;
detection.prev_fingerprint = fp;
assert!(detection.changed());
let fp2 = compute_frame_fingerprint(&state);
detection.frame_changed = fp2 != detection.prev_fingerprint;
detection.prev_fingerprint = fp2;
assert!(!detection.changed());
assert!(frame_unchanged(&detection, &state));
}
#[test]
fn visualization_value_update_changes_fingerprint() {
let mut state = MapState::new();
state.set_grid_extrusion(
"surface",
rustial_engine::GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 1, 1, 10.0, 10.0),
rustial_engine::ScalarField2D::from_data(1, 1, vec![1.0]),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 0.8],
},
]),
rustial_engine::ExtrusionParams {
height_scale: 2.0,
base_meters: 1.0,
},
);
state.update();
let fp1 = compute_frame_fingerprint(&state);
let mut field = rustial_engine::ScalarField2D::from_data(1, 1, vec![1.0]);
field.update_values(vec![3.0]);
state.set_grid_extrusion(
"surface",
rustial_engine::GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 1, 1, 10.0, 10.0),
field,
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 0.8],
},
]),
rustial_engine::ExtrusionParams {
height_scale: 2.0,
base_meters: 1.0,
},
);
state.update();
let fp2 = compute_frame_fingerprint(&state);
assert_ne!(fp1, fp2);
}
#[test]
fn column_overlay_value_update_changes_fingerprint() {
let mut state = MapState::new();
state.set_instanced_columns(
"columns",
rustial_engine::ColumnInstanceSet::new(vec![rustial_engine::ColumnInstance::new(
GeoCoord::from_lat_lon(0.0, 0.0),
10.0,
5.0,
)
.with_pick_id(7)]),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 0.8],
},
]),
);
state.update();
let fp1 = compute_frame_fingerprint(&state);
state.set_instanced_columns(
"columns",
rustial_engine::ColumnInstanceSet::new(vec![rustial_engine::ColumnInstance::new(
GeoCoord::from_lat_lon(0.0, 0.0),
25.0,
5.0,
)
.with_pick_id(7)]),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 0.8],
},
]),
);
state.update();
let fp2 = compute_frame_fingerprint(&state);
assert_ne!(fp1, fp2);
}
}