Skip to main content
Verticopolis engineGitHub

verticopolis_engine/view/
reader.rs

1//! The dynamic frame: what a view reads every rendered frame. The header,
2//! the crowd's people and every transport's cars go in full each time; the
3//! rooms go only when their frame fields changed since this reader's last
4//! frame. The reader finds them in the tower's mark log (`marks.rs`), which
5//! the setters of those fields write, so a frame visits only the rooms that
6//! changed (a full frame aside) and no floor or lobby tile (their fields
7//! move only with the revision, which the structural snapshot covers).
8use std::sync::Arc;
9
10use super::layout::{HEADER, PERSON_RECORD, UNIT_RECORD};
11use crate::frame::{person_state_code, push_transports, transports_len, unit_fields, weather_code};
12use crate::sim::Simulation;
13use crate::tower::Unit;
14
15/// Reads the view's frame for one game, sending only what changed since its
16/// last frame. Keep one per game: a host that loads another game starts a
17/// new reader. A reader handed another game sends a full frame.
18#[derive(Default)]
19pub struct ViewReader {
20    sent: Option<(u64, i64, usize)>,
21    last: Vec<[u64; 5]>,
22    census: Vec<i64>,
23    population: i64,
24    read: u64,
25    seq: u64,
26    marked: Vec<usize>,
27    changed: Vec<f64>,
28}
29
30impl ViewReader {
31    /// A reader that has sent nothing, so its first frame is full.
32    pub fn new() -> ViewReader {
33        ViewReader::default()
34    }
35
36    /// The dynamic frame as one flat number array.
37    ///
38    /// Header (`HEADER` slots, see `layout::header`): minutes, tower
39    /// revision, meta revision, meal overlay revision, logSeq, money, star,
40    /// the unlocked kinds (bit k set when the kind with code k is unlocked),
41    /// the population (as `Simulation::population` counts it), weather, the
42    /// Santa, explosion, thief, treasure and VIP effects and the event
43    /// counts as `frameView` carries them, a pending choice (0 or 1),
44    /// onHourRuns, the room count the slots index, the full flag, this
45    /// reader's frame number (1 for its first), then the people, room record
46    /// and transport counts.
47    ///
48    /// Then `people` records of `PERSON_RECORD`, every person in the crowd
49    /// as [`crate::frame::frame_view`] sends them: id, seed, staff (0 or 1), state, floor, x,
50    /// fy, wait. Then `units` room records of `UNIT_RECORD`:
51    /// the room's slot (its position in the structural snapshot's room list),
52    /// then state, occupants, customersIn, hotelCustomersIn, outForMeal (an
53    /// absent counter is -1). Then every transport as [`crate::frame::frame_view`] writes it.
54    ///
55    /// The room records are every room, in slot order, with the full flag
56    /// set, when `full` is true, on this reader's first frame, or when the
57    /// revision or the unit count moved since its last frame (the slots then
58    /// index a new structure). Otherwise they are the rooms whose fields
59    /// changed since its last frame, in slot order, a field compared bit for
60    /// bit.
61    ///
62    /// A full frame makes one pass over the rooms index, which happens once
63    /// per revision move (a command), on a new tower, or when the host asks.
64    /// Any other frame visits the rooms the mark log names since this
65    /// reader's last frame, each once, plus a binary search of the rooms
66    /// index for each; with nothing marked it visits no room. When the marks
67    /// it reads reach half the rooms (an hour can mark nearly all of them),
68    /// or the log has dropped marks this reader never read (it fell more than
69    /// `MARK_CAP` marks behind), the frame compares every room once instead,
70    /// which sends the same rooms for less (`compare_all`). The population
71    /// is a running total that each visited room corrects. One allocation,
72    /// made at the frame's size.
73    ///
74    /// The engine's own writes go through the setters, which mark the room.
75    /// A native host that writes a unit's frame fields or residents itself,
76    /// outside the commands and the tick, leaves the room unmarked, so its
77    /// next frame passes `full`.
78    pub fn frame(&mut self, sim: &Simulation, full: bool) -> Vec<f64> {
79        let tower = &sim.tower;
80        let rooms: Arc<[usize]> = tower.room_index();
81        let marks = &tower.view.marks;
82        let key = (marks.id(), tower.revision, tower.units.len());
83        let full = full || self.sent != Some(key) || self.last.len() != rooms.len();
84        self.changed.clear();
85        if full {
86            self.last.clear();
87            self.last.reserve(rooms.len());
88            self.census.clear();
89            self.census.reserve(rooms.len());
90            self.changed.reserve(rooms.len() * UNIT_RECORD);
91            self.population = 0;
92            for (slot, &i) in rooms.iter().enumerate() {
93                let u = &tower.units[i];
94                let fields = unit_fields(u);
95                self.last.push(fields.map(f64::to_bits));
96                let census = u.census_share();
97                self.census.push(census);
98                self.population += census;
99                self.changed.push(slot as f64);
100                self.changed.extend_from_slice(&fields);
101            }
102        } else {
103            let mut marked = std::mem::take(&mut self.marked);
104            marked.clear();
105            // The count decides first, so a frame that compares every room
106            // copies no mark.
107            let few = marks
108                .unread(self.read)
109                .is_some_and(|n| !compare_all(n, rooms.len()));
110            if few && marks.read_since(self.read, &mut marked) {
111                // Unit-list order is the rooms index's order, so sorted
112                // indices give the slots in order.
113                marked.sort_unstable();
114                marked.dedup();
115                for &i in &marked {
116                    if let Ok(slot) = rooms.binary_search(&i) {
117                        self.update(slot, &tower.units[i]);
118                    }
119                }
120            } else {
121                for (slot, &i) in rooms.iter().enumerate() {
122                    self.update(slot, &tower.units[i]);
123                }
124            }
125            self.marked = marked;
126        }
127        self.read = marks.end();
128        self.sent = Some(key);
129        self.seq += 1;
130        let population = self.population;
131        let people = &sim.crowd.people;
132        let size = HEADER
133            + people.len() * PERSON_RECORD
134            + self.changed.len()
135            + transports_len(&tower.transports);
136        let mut v = Vec::with_capacity(size);
137        let fx = &sim.fx;
138        let clock = &sim.clock;
139        v.extend_from_slice(&[
140            clock.minutes,
141            tower.revision as f64,
142            tower.meta_revision as f64,
143            tower.meal_overlay_revision as f64,
144            sim.log_seq as f64,
145            sim.money,
146            sim.star as f64,
147            unlocked_mask(sim),
148            population as f64,
149            weather_code(sim.weather),
150            fx.santa_seq as f64,
151            fx.explosion.seq as f64,
152            fx.explosion.floor as f64,
153            fx.explosion.x,
154            fx.thief.seq as f64,
155            if fx.thief.caught { 1.0 } else { 0.0 },
156            fx.thief.floor as f64,
157            fx.treasure.seq as f64,
158            fx.treasure.floor as f64,
159            fx.treasure.x,
160            fx.vip_seq as f64,
161            sim.events.fires_started as f64,
162            sim.events.fires_gut_rooms as f64,
163            sim.events.bombs_detonated as f64,
164            if sim.events.pending.is_some() {
165                1.0
166            } else {
167                0.0
168            },
169            sim.on_hour_runs as f64,
170            rooms.len() as f64,
171            if full { 1.0 } else { 0.0 },
172            self.seq as f64,
173            people.len() as f64,
174            (self.changed.len() / UNIT_RECORD) as f64,
175            tower.transports.len() as f64,
176            clock.day() as f64,
177            clock.day_of_week() as f64,
178            if clock.is_weekend() { 1.0 } else { 0.0 },
179            clock.quarter() as f64,
180            clock.year() as f64,
181            clock.calendar.week_days as f64,
182            clock.calendar.weekend_days as f64,
183            sim.events.bomb.as_ref().map_or(0.0, |b| b.seq as f64),
184        ]);
185        debug_assert_eq!(v.len(), HEADER);
186        for p in people {
187            v.extend_from_slice(&[
188                p.id as f64,
189                p.seed as f64,
190                if p.staff { 1.0 } else { 0.0 },
191                person_state_code(p.state),
192                p.floor as f64,
193                p.x,
194                p.fy,
195                p.wait,
196            ]);
197        }
198        v.extend_from_slice(&self.changed);
199        push_transports(&mut v, &tower.transports);
200        debug_assert_eq!(v.len(), size);
201        v
202    }
203
204    /// Bring the room in `slot` up to date: correct the population by the
205    /// change in its census, and queue its record when a frame field
206    /// changed, compared bit for bit.
207    fn update(&mut self, slot: usize, u: &Unit) {
208        let census = u.census_share();
209        self.population += census - self.census[slot];
210        self.census[slot] = census;
211        let fields = unit_fields(u);
212        let bits = fields.map(f64::to_bits);
213        let last = &mut self.last[slot];
214        if (0..5).any(|k| bits[k] != last[k]) {
215            *last = bits;
216            self.changed.push(slot as f64);
217            self.changed.extend_from_slice(&fields);
218        }
219    }
220}
221
222/// Whether a frame that read `marks` marks (repeats counted, before the
223/// sort) compares every room instead of finding each marked one. From
224/// about half the rooms the sort and a binary search per mark cost more
225/// than one compare over the rooms. The review measured two frames after an
226/// hour on the large tower (2,900 rooms): 2,886 marks cost 875K
227/// instructions on the marks and 489K with this threshold, the compare's
228/// cost; 1,377 marks cost 406.6K on the marks against 339.4K for a compare.
229/// The compare's cost moves with the rooms that changed, so no one share of
230/// the rooms breaks even on every frame, and just below half the marks cost
231/// up to 1.2 times a compare on the frames measured. Both branches send the
232/// same rooms, since every write of a frame field marks. The census tally
233/// (`tally.rs`) uses the same threshold with no sort, at one constant visit
234/// per mark, so either of its branches costs at most about one visit per
235/// room.
236pub(super) fn compare_all(marks: usize, rooms: usize) -> bool {
237    marks * 2 >= rooms
238}
239
240/// The unlocked kinds as a bitmask by kind code: bit k is set when the kind
241/// with code k (`Kind as usize`) is unlocked (`Simulation::is_unlocked`).
242/// A double holds 53 bits exactly, and `layout_tests.rs` keeps the catalog
243/// inside them.
244fn unlocked_mask(sim: &Simulation) -> f64 {
245    let mut mask = 0u64;
246    for (k, f) in crate::facilities::FACILITIES.iter().enumerate() {
247        if sim.is_unlocked(f.kind) {
248            mask |= 1 << k;
249        }
250    }
251    mask as f64
252}