use rustial_math::TileId;
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SourcePriority {
Raster,
Vector,
Terrain,
}
impl SourcePriority {
fn default_weight(self) -> u32 {
match self {
Self::Raster => 3,
Self::Vector => 2,
Self::Terrain => 1,
}
}
}
#[derive(Debug, Clone)]
pub struct CoordinatorConfig {
pub global_request_budget: usize,
pub weights: Option<[u32; 3]>,
}
impl Default for CoordinatorConfig {
fn default() -> Self {
Self {
global_request_budget: 32,
weights: None,
}
}
}
#[derive(Debug, Default)]
struct SourceSlot {
demand: usize,
budget: usize,
issued: usize,
pending_demand: usize,
desired: HashSet<TileId>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CoordinatorStats {
pub budget_total: usize,
pub budget_raster: usize,
pub budget_vector: usize,
pub budget_terrain: usize,
pub shared_tile_count: usize,
pub unique_desired_tiles: usize,
}
#[derive(Debug)]
pub struct TileRequestCoordinator {
config: CoordinatorConfig,
slots: [SourceSlot; 3],
stats: CoordinatorStats,
}
impl TileRequestCoordinator {
pub fn new(config: CoordinatorConfig) -> Self {
Self {
config,
slots: Default::default(),
stats: CoordinatorStats::default(),
}
}
pub fn budget_for(&self, priority: SourcePriority) -> usize {
if self.config.global_request_budget == 0 {
return usize::MAX;
}
self.slots[Self::idx(priority)].budget
}
pub fn report(
&mut self,
priority: SourcePriority,
desired: HashSet<TileId>,
issued: usize,
pending_demand: usize,
) {
let slot = &mut self.slots[Self::idx(priority)];
slot.desired = desired;
slot.issued = issued;
slot.pending_demand = pending_demand;
}
pub fn report_demand(&mut self, priority: SourcePriority, demand: usize) {
self.slots[Self::idx(priority)].demand = demand;
}
#[allow(clippy::needless_range_loop)]
pub fn begin_frame(&mut self) {
if self.config.global_request_budget == 0 {
for slot in &mut self.slots {
slot.budget = usize::MAX;
slot.issued = 0;
slot.desired.clear();
}
return;
}
let weights = self.config.weights.unwrap_or([
SourcePriority::Raster.default_weight(),
SourcePriority::Vector.default_weight(),
SourcePriority::Terrain.default_weight(),
]);
let total_weight: u32 = weights.iter().sum();
let budget = self.config.global_request_budget;
let mut budgets = [0usize; 3];
let mut remainder = budget;
if total_weight > 0 {
for (i, &w) in weights.iter().enumerate() {
budgets[i] = budget * w as usize / total_weight as usize;
remainder -= budgets[i];
}
for i in 0..3 {
if remainder == 0 {
break;
}
if self.slots[i].demand > 0 {
budgets[i] += 1;
remainder -= 1;
}
}
}
let mut excess = 0usize;
for i in 0..3 {
if self.slots[i].demand == 0 {
excess += budgets[i];
budgets[i] = 0;
}
}
if excess > 0 {
let needy: Vec<usize> = (0..3).filter(|&i| self.slots[i].demand > 0).collect();
let needy_weight: u32 = needy.iter().map(|&i| weights[i]).sum();
if needy_weight > 0 {
let mut leftover = excess;
for &i in &needy {
let share = excess * weights[i] as usize / needy_weight as usize;
budgets[i] += share;
leftover -= share;
}
if leftover > 0 {
if let Some(&first) = needy.first() {
budgets[first] += leftover;
}
}
}
}
for (i, slot) in self.slots.iter_mut().enumerate() {
slot.budget = budgets[i];
slot.issued = 0;
slot.desired.clear();
}
}
pub fn finish_frame(&mut self) {
let mut all_tiles: HashSet<TileId> = HashSet::new();
let mut total_per_source = 0usize;
for slot in &self.slots {
total_per_source += slot.desired.len();
all_tiles.extend(slot.desired.iter());
}
let unique = all_tiles.len();
let shared = total_per_source.saturating_sub(unique);
for slot in &mut self.slots {
slot.demand = slot.issued.max(slot.pending_demand);
}
self.stats = CoordinatorStats {
budget_total: self.config.global_request_budget,
budget_raster: self.slots[0].budget,
budget_vector: self.slots[1].budget,
budget_terrain: self.slots[2].budget,
shared_tile_count: shared,
unique_desired_tiles: unique,
};
}
pub fn stats(&self) -> &CoordinatorStats {
&self.stats
}
pub fn config(&self) -> &CoordinatorConfig {
&self.config
}
pub fn set_config(&mut self, config: CoordinatorConfig) {
self.config = config;
}
#[inline]
fn idx(priority: SourcePriority) -> usize {
match priority {
SourcePriority::Raster => 0,
SourcePriority::Vector => 1,
SourcePriority::Terrain => 2,
}
}
}
impl Default for TileRequestCoordinator {
fn default() -> Self {
Self::new(CoordinatorConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_budget_allocation_respects_weights() {
let mut coord = TileRequestCoordinator::default();
coord.slots[0].demand = 10; coord.slots[1].demand = 10; coord.slots[2].demand = 10;
coord.begin_frame();
assert_eq!(coord.budget_for(SourcePriority::Raster), 17);
assert_eq!(coord.budget_for(SourcePriority::Vector), 10);
assert_eq!(coord.budget_for(SourcePriority::Terrain), 5);
let total = coord.budget_for(SourcePriority::Raster)
+ coord.budget_for(SourcePriority::Vector)
+ coord.budget_for(SourcePriority::Terrain);
assert_eq!(total, 32);
}
#[test]
fn unused_budget_redistributed_to_needy_sources() {
let mut coord = TileRequestCoordinator::default();
coord.slots[0].demand = 20; coord.slots[1].demand = 0; coord.slots[2].demand = 0;
coord.begin_frame();
assert_eq!(coord.budget_for(SourcePriority::Raster), 32);
assert_eq!(coord.budget_for(SourcePriority::Vector), 0);
assert_eq!(coord.budget_for(SourcePriority::Terrain), 0);
}
#[test]
fn zero_budget_disables_coordination() {
let config = CoordinatorConfig {
global_request_budget: 0,
..Default::default()
};
let mut coord = TileRequestCoordinator::new(config);
coord.begin_frame();
assert_eq!(coord.budget_for(SourcePriority::Raster), usize::MAX);
assert_eq!(coord.budget_for(SourcePriority::Vector), usize::MAX);
assert_eq!(coord.budget_for(SourcePriority::Terrain), usize::MAX);
}
#[test]
fn shared_tile_count_tracks_cross_source_overlap() {
let mut coord = TileRequestCoordinator::default();
coord.begin_frame();
let tile_a = TileId::new(5, 10, 12);
let tile_b = TileId::new(5, 11, 12);
let tile_c = TileId::new(5, 12, 12);
coord.report(
SourcePriority::Raster,
[tile_a, tile_b].into_iter().collect(),
2,
0,
);
coord.report(
SourcePriority::Vector,
[tile_b, tile_c].into_iter().collect(),
2,
0,
);
coord.finish_frame();
let stats = coord.stats();
assert_eq!(stats.unique_desired_tiles, 3);
assert_eq!(stats.shared_tile_count, 1);
}
#[test]
fn finish_frame_updates_demand_for_next_frame() {
let mut coord = TileRequestCoordinator::default();
coord.slots[0].demand = 10;
coord.slots[1].demand = 5;
coord.slots[2].demand = 3;
coord.begin_frame();
coord.report(SourcePriority::Raster, HashSet::new(), 8, 0);
coord.report(SourcePriority::Vector, HashSet::new(), 3, 0);
coord.report(SourcePriority::Terrain, HashSet::new(), 0, 0);
coord.finish_frame();
coord.begin_frame();
assert_eq!(coord.budget_for(SourcePriority::Terrain), 0);
let raster_budget = coord.budget_for(SourcePriority::Raster);
let vector_budget = coord.budget_for(SourcePriority::Vector);
assert_eq!(raster_budget + vector_budget, 32);
assert!(raster_budget > vector_budget);
}
#[test]
fn custom_weights_override_defaults() {
let config = CoordinatorConfig {
global_request_budget: 20,
weights: Some([1, 1, 1]), };
let mut coord = TileRequestCoordinator::new(config);
coord.slots[0].demand = 10;
coord.slots[1].demand = 10;
coord.slots[2].demand = 10;
coord.begin_frame();
let r = coord.budget_for(SourcePriority::Raster);
let v = coord.budget_for(SourcePriority::Vector);
let t = coord.budget_for(SourcePriority::Terrain);
assert_eq!(r + v + t, 20);
assert!(r.abs_diff(v) <= 1);
assert!(v.abs_diff(t) <= 1);
}
}