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}