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
245
246
247
248
249
//! How the engine paces its loops and sizes its thread pools: the places a
//! mod changes to make the game use less CPU.
//!
//! - [`Load::on_loop_wait`]: the engine's event loop, about to wait on its handles.
//! It turns the time left before its deadline into whole milliseconds,
//! truncating, so with less than a millisecond left it waits 0 ms, again
//! and again, until the deadline passes.
//! - [`Load::on_server_wait`]: the server's game loop picking how long it waits
//! between its passes.
//! - [`Load::cap_task_workers`]: the most worker threads each task group starts
//! with, which the engine sizes from the logical CPUs it sees.
//!
//! The loader finds each place on the running build and owns its hook; a
//! place this build lacks is reported, never guessed.
//!
//! # Examples
//!
//! ```
//! fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
//! let found = load.on_loop_wait(|wait| {
//! if wait.ms == 0 && wait.left_ns > 0 {
//! // Sleep `left_ns` here instead of letting the game poll.
//! }
//! })?;
//! if !found.handles {
//! enshwick::warn!("the event loop's wait was not found");
//! }
//! Ok(())
//! }
//! ```
use core::ffi::c_void;
use crate::abi::{LoopWaitData, LoopWaitSites};
use crate::{Load, Off};
/// The server loop's wait with nobody online.
pub const SERVER_IDLE_WAIT_NS: u64 = 40_000_000;
/// The server loop's wait with players online.
pub const SERVER_ONLINE_WAIT_NS: u64 = 1_000_000;
/// The server loop's wait after a pass longer than 14 ms.
pub const SERVER_SLOW_WAIT_NS: u64 = 100_000;
/// A wait of the engine's event loop on its handles, about to happen.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct LoopWait {
/// What the game is about to wait, in milliseconds: the time left,
/// truncated.
pub ms: u32,
/// The time left before the loop's deadline, in nanoseconds.
pub left_ns: i64,
}
/// Which of the event loop's waits [`Load::on_loop_wait`] found and hooked.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct LoopWaits {
/// The wait on handles, on the server and the client.
pub handles: bool,
/// The client's wait on handles and window messages.
pub messages: bool,
}
unsafe extern "system" fn run_loop_wait<F>(wait: *const LoopWaitData, user: *mut c_void)
where
F: Fn(LoopWait),
{
// SAFETY: the loader passes a valid wait for the call; `as_ref` turns
// away null.
let Some(wait) = (unsafe { wait.as_ref() }) else {
return;
};
let call = LoopWait {
ms: wait.ms,
left_ns: wait.left_ns,
};
// SAFETY: `user` is the leaked `F` this function was registered with.
let handler = unsafe { &*user.cast::<F>() };
crate::host::guard("panic in a loop wait handler", || (), || handler(call));
}
unsafe extern "system" fn run_server_wait<F>(wait_ns: *mut u64, user: *mut c_void)
where
F: Fn(&mut u64),
{
// SAFETY: the loader passes the wait for the call, writable; `as_mut`
// turns away null.
let Some(wait_ns) = (unsafe { wait_ns.as_mut() }) else {
return;
};
// SAFETY: `user` is the leaked `F` this function was registered with.
let handler = unsafe { &*user.cast::<F>() };
crate::host::guard("panic in a server wait handler", || (), || handler(wait_ns));
}
/// The most worker threads the task groups start with; 0 leaves a group
/// as the game sizes it.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct TaskWorkers {
/// The default group, which also sizes the ECS task queue.
pub default: u8,
/// The background group: streaming, saves and other slow work.
pub background: u8,
}
impl TaskWorkers {
/// The same cap for both groups.
///
/// # Examples
///
/// ```
/// let caps = enshwick::pacing::TaskWorkers::at_most(4);
/// assert_eq!((caps.default, caps.background), (4, 4));
/// ```
#[must_use]
pub const fn at_most(workers: u8) -> Self {
Self {
default: workers,
background: workers,
}
}
}
impl Load<'_> {
/// Runs `handler` before every wait of the engine's event loop on its
/// handles, on the thread that waits: the app's main loop, the server's
/// game loop and its session, and on the client its message wait too. A
/// handler that sleeps must never sleep past `left_ns`, and must leave the
/// waiting to the game, so it sees every event. Allowed during
/// [`Mod::init`](crate::Mod::init) only.
///
/// # Errors
///
/// [`Off`] when the loader refuses a hook. A wait this build lacks is
/// reported as not found in the result.
///
/// # Examples
///
/// ```
/// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
/// let found = load.on_loop_wait(|wait| {
/// let _ = (wait.ms, wait.left_ns);
/// })?;
/// let _ = found.messages;
/// Ok(())
/// }
/// ```
pub fn on_loop_wait<F>(&mut self, handler: F) -> Result<LoopWaits, Off>
where
F: Fn(LoopWait) + Send + Sync + 'static,
{
let host = self.host();
let mut sites = LoopWaitSites::NONE;
crate::load::hand_over(
handler,
|user| (host.api().loop_wait)(host.handle(), run_loop_wait::<F>, user, &raw mut sites),
"the loop wait handler",
)?;
Ok(LoopWaits {
handles: sites.contains(LoopWaitSites::HANDLES),
messages: sites.contains(LoopWaitSites::MESSAGES),
})
}
/// Runs `handler` where the server's game loop has picked its wait between
/// two passes, in nanoseconds: [`SERVER_IDLE_WAIT_NS`],
/// [`SERVER_ONLINE_WAIT_NS`] or [`SERVER_SLOW_WAIT_NS`]. The handler may
/// change it; the loop waits on its handles for that long, so events still
/// end the wait early. On the server's game thread, once a pass; `false`
/// when this build lacks the site. Allowed during
/// [`Mod::init`](crate::Mod::init) only.
///
/// # Errors
///
/// [`Off`] when the loader refuses the hook.
///
/// # Examples
///
/// ```
/// use enshwick::pacing;
///
/// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
/// let hooked = load.on_server_wait(|wait_ns| {
/// if *wait_ns == pacing::SERVER_ONLINE_WAIT_NS {
/// *wait_ns = 2 * pacing::SERVER_ONLINE_WAIT_NS;
/// }
/// })?;
/// let _ = hooked;
/// Ok(())
/// }
/// ```
pub fn on_server_wait<F>(&mut self, handler: F) -> Result<bool, Off>
where
F: Fn(&mut u64) + Send + Sync + 'static,
{
let host = self.host();
let mut found = 0;
crate::load::hand_over(
handler,
|user| {
(host.api().server_wait)(host.handle(), run_server_wait::<F>, user, &raw mut found)
},
"the server wait handler",
)?;
Ok(found != 0)
}
/// Caps the worker threads of the task groups as the task system starts.
/// The engine sizes them from the logical CPUs it sees, which under Wine
/// are the host's, whatever a container allows. Only the dedicated server
/// starts its task system after the loader arms hooks; on the client it has
/// started already and the result is `false`, as it is when this build lacks
/// the site or both caps are 0. The game's log shows the counts it started with:
/// `[task] Group 'Default' Worker:…`. Allowed during
/// [`Mod::init`](crate::Mod::init) only.
///
/// # Errors
///
/// [`Off`] when the loader refuses a hook.
///
/// # Examples
///
/// ```
/// use enshwick::pacing::TaskWorkers;
///
/// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
/// if !load.cap_task_workers(TaskWorkers::at_most(4))? {
/// enshwick::info!("task workers stay as the game sizes them");
/// }
/// Ok(())
/// }
/// ```
pub fn cap_task_workers(&mut self, caps: TaskWorkers) -> Result<bool, Off> {
if caps == TaskWorkers::default() {
return Ok(false);
}
let host = self.host();
let mut found = 0;
let status = (host.api().task_workers)(
host.handle(),
u32::from(caps.default),
u32::from(caps.background),
&raw mut found,
);
crate::load::ok(status, found != 0, || "the task worker caps".to_owned())
}
}