1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
//! Build-mode vocabulary: which diagnostics a build compiles in.
//!
//! retroglyph emits diagnostics that exist purely to shorten the debugging loop: a warning that
//! a sprite is bigger than the cells reserved for it, a warning that a tint was set on a cell
//! that resolved to a font glyph rather than a sprite. Each one costs something to produce (a
//! formatted message, and usually a side table so a 60fps redraw loop reports each offender once
//! instead of every frame), and none of it is worth anything in a shipped game, where nobody is
//! reading the log.
//!
//! [`BuildMode::CURRENT`](crate::dev::BuildMode::CURRENT) names which kind of build this is, and [`dev_only!`] gates a block on
//! it. In a release build the const is `false`, the branch folds away, and everything inside it
//! (message strings, the bookkeeping that dedupes them) is dropped as dead code.
//!
//! ```
//! use retroglyph_core::dev_only;
//!
//! # fn report(_: &str, _: usize) {}
//! # let cache_misses = 3;
//! dev_only!({
//! if cache_misses > 0 {
//! // Costs nothing in a release build: neither the check nor the message survives.
//! report("glyphs missed the sprite cache", cache_misses);
//! }
//! });
//! ```
//!
//! # Two modes, not three
//!
//! Engines that own their whole toolchain usually expose three build modes. Flutter's
//! `debug`/`profile`/`release` is the clearest version: `debug` is unoptimized with every
//! assertion live, `profile` is optimized but keeps enough instrumentation to attribute a frame
//! budget, and `release` is what ships.
//!
//! Cargo has no `profile` mode in that sense. A profiling build is a release build that keeps
//! debug symbols (`[profile.profiling] inherits = "release"`, plus `debug = true`), and it is
//! *supposed* to be one: measuring a build whose diagnostics differ from the shipped build
//! measures the wrong program. So there are two modes here, and a profiling build resolves to
//! [`Release`](crate::dev::BuildMode::Release).
//!
//! That is also why the gate is written as "is this a dev build" rather than "is this not a
//! release build". Flutter's own guidance on its `kReleaseMode` constant is to prefer `kDebugMode`
//! or `assert` precisely because gating on *not release* is what makes a profile build behave
//! unlike the release build it is meant to predict.
//!
//! # How a mode is chosen
//!
//! | Build | [`BuildMode::CURRENT`](crate::dev::BuildMode::CURRENT) |
//! | --- | --- |
//! | `cargo build`, `cargo test`, `cargo run` | [`Dev`](crate::dev::BuildMode::Dev) |
//! | `cargo build --release` | [`Release`](crate::dev::BuildMode::Release) |
//! | a profiling profile inheriting `release` | [`Release`](crate::dev::BuildMode::Release) |
//! | any build with the `dev` feature on | [`Dev`](crate::dev::BuildMode::Dev) |
//! | any build with `-C debug-assertions=on` | [`Dev`](crate::dev::BuildMode::Dev) |
//!
//! The default signal is `debug_assertions`, which Cargo turns on for the `dev` profile and off
//! for `release`. It follows whichever profile the consumer built with, so a game gets
//! diagnostics from `cargo run` and none from `cargo run --release` without configuring anything.
//!
//! The `dev` feature forces [`Dev`](crate::dev::BuildMode::Dev) on regardless, for an optimized build that
//! still reports. This is the equivalent of Unity's "Development Build" checkbox or Bevy's `dev`
//! feature: release codegen, because an unoptimized build of a renderer is too slow to reproduce
//! anything frame-dependent, but with the instrumentation left in.
//!
//! # Turning diagnostics off in a dev build
//!
//! There is no feature for this. Cargo features are additive, so a `no-dev` feature
//! would be silently defeated by any other crate in the graph that wanted diagnostics.
//!
//! Every diagnostic in this workspace goes through the `log` crate, so the two working controls
//! are the consumer's own log filter at runtime, and `log`'s `max_level_*` /
//! `release_max_level_*` features, which drop the calls at compile time. Those cut deeper than
//! this module does: they apply to every `log` user in the graph, not just retroglyph.
//!
//! # Load-time versus per-frame
//!
//! Not every `log::warn!` in this workspace goes through [`dev_only!`]. The rule is where the
//! call sits, not what category of mistake it reports: a diagnostic reachable from a redraw loop
//! is [`dev_only!`]-gated, because at 60fps an ungated one reformats its message and grows its
//! `seen` dedup table every frame the condition holds. A diagnostic reachable only from a one-time
//! setup path, such as decoding a tileset, has neither cost to save by gating it, and it may be
//! reporting an asset or config mistake a consumer wants to see even in a shipped build. So it
//! stays ungated. `retroglyph-window`'s tileset codepoint-collision warning is the example: it
//! fires at most once per tileset load, not once per frame.
/// Which diagnostics this build compiles in.
///
/// Read [`CURRENT`](Self::CURRENT) for this build's mode, or use [`dev_only!`](crate::dev_only)
/// to gate a block on it. See the [module docs](self) for how a mode is chosen and why there are
/// two of them rather than three.
/// Whether this build compiles in development diagnostics: [`BuildMode::CURRENT`](crate::dev::BuildMode::CURRENT) as a `bool`.
///
/// Prefer [`dev_only!`](crate::dev_only) for gating a block. Reach for this constant directly
/// when the shape of the code makes a macro awkward, such as an early return or a struct field
/// that only one mode populates.
pub const DEV: bool = CURRENT.is_dev;
/// Runs `body` only in a build that compiles in development diagnostics.
///
/// Expands to `if DEV { body }`. Because [`DEV`](crate::dev::DEV) is a `const`, a release build folds the branch
/// away and drops `body` with it, including any message strings and bookkeeping it alone
/// references.
///
/// `body` is type-checked in every mode. That is the point: a diagnostic that only compiles on
/// one profile rots, and the rot surfaces as a broken release build. The cost is that `body` may
/// not reference items that themselves exist only in a dev build.
///
/// Control flow escapes the block on one profile only. A `return`, `?`, `break`, or `continue`
/// inside `body` runs in a dev build and is skipped entirely in a release build, so the
/// surrounding function must be correct when the block does nothing. Confine `body` to
/// diagnostics and their bookkeeping; if the enclosing function's result depends on it, the
/// profiles disagree.
///
/// # Examples
///
/// ```
/// use retroglyph_core::dev_only;
///
/// # fn warn_overflow(_: (u32, u32), _: (u32, u32)) {}
/// let sprite_px = (32, 32);
/// let cell_px = (16, 16);
///
/// dev_only!({
/// if sprite_px > cell_px {
/// warn_overflow(sprite_px, cell_px);
/// }
/// });
/// ```
///
/// The block form is not required; any statements work.
///
/// ```
/// # use retroglyph_core::dev_only;
/// # let mut misses = 0;
/// dev_only!(misses += 1;);
/// ```