tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
schedule.h
1#pragma once
2
3#include <tess/core/assert.h>
4#include <tess/core/config.h>
5#include <tess/core/fail_fast.h>
6#include <tess/diagnostics/trace.h>
7#include <tess/sim/event_stream.h>
8#include <tess/sim/time.h>
9#include <tess/storage/metadata_types.h>
10
11#include <cstddef>
12#include <cstdint>
13#include <string_view>
14#include <type_traits>
15#include <vector>
16
17// Ordered phases of type-erased tasks driven by cadences
18// that are pure functions of the fixed-tick counter and per-task pending
19// dirty/event masks. The schedule itself never touches a world -- trigger
20// bits are fed to it explicitly -- so "no hidden full-world scans" holds by
21// construction. World-typed work lives in task objects the caller owns and
22// registers by reference; type erasure is a function pointer plus a context
23// pointer (no std::function, no allocation on dispatch).
24//
25// Threading: a Schedule is externally synchronized like every tess scratch.
26// notify_dirty, notify_events, and request_run are frame-owner-thread calls and
27// must never be made from queued-operation callbacks (those may run on pool
28// workers); worker-produced triggers flow through task-result masks.
29//
30// Reentrancy: task bodies may call notify_dirty, notify_events, request_run,
31// and set_enabled (field writes on address-stable storage, with the documented
32// immediate-merge semantics). They must NOT call add_task, reserve_tasks, or
33// run_tick -- registration/capacity changes after seal() could invalidate the
34// task array mid-iteration, and a nested tick would double-advance every
35// cadence. These violations fail fast in every build.
36namespace tess {
37
39enum class CadenceKind : std::uint8_t {
40 EveryTick,
41 EveryN,
42 OnDirty,
43 OnEvent,
44 Background,
45 Manual,
46};
47
48// Deterministic background bound: a due background task is offered at most
49// max_items work units per run and reports how many it consumed plus
50// whether work remains. There is deliberately no wall-clock budget --
51// a time valve would make tick outcomes nondeterministic, and every
52// consumer bound is expressible in items; it returns with its first real
53// consumer.
56 std::uint32_t max_items = 1;
57};
58
60struct Cadence {
61 CadenceKind kind = CadenceKind::EveryTick;
62 std::uint32_t every_n = 1;
63 DirtyMask dirty_mask = {};
64 BackgroundBudget budget{};
65 std::uint32_t event_mask = 0;
66
67 [[nodiscard]] static constexpr auto every_tick() noexcept -> Cadence {
68 return Cadence{};
69 }
70
71 [[nodiscard]] static constexpr auto every_ticks(std::uint32_t n) noexcept
72 -> Cadence {
73 return Cadence{CadenceKind::EveryN, n == 0 ? 1u : n, {}, {}, 0};
74 }
75
76 [[nodiscard]] static constexpr auto on_dirty(DirtyMask mask) noexcept
77 -> Cadence {
78 return Cadence{CadenceKind::OnDirty, 1, mask, {}, 0};
79 }
80
81 [[nodiscard]] static constexpr auto on_event(std::uint32_t mask) noexcept
82 -> Cadence {
83 return Cadence{CadenceKind::OnEvent, 1, {}, {}, mask};
84 }
85
86 [[nodiscard]] static constexpr auto background(
87 BackgroundBudget budget) noexcept -> Cadence {
88 return Cadence{
89 CadenceKind::Background,
90 1,
91 {},
92 BackgroundBudget{budget.max_items == 0 ? 1u : budget.max_items},
93 0};
94 }
95
96 [[nodiscard]] static constexpr auto manual() noexcept -> Cadence {
97 return Cadence{CadenceKind::Manual, 1, {}, {}, 0};
98 }
99};
100
101// Fixed phase list, executed in declaration order every tick. Tasks run in
102// registration order within a phase. The set matches the simulation TDD's
103// phase vocabulary; custom phase lists are deferred until a consumer needs
104// one.
106enum class SimPhase : std::uint8_t {
107 Input,
108 PreUpdate,
109 AI,
110 Pathing,
111 Movement,
112 Commit,
113 Topology,
114 Fields,
115 Background,
116 RenderDelta,
117 Diagnostics,
118 Count,
119};
120
123 SimClock clock{};
124 // OnDirty: the bits (within the task's own mask) that made it due; they
125 // are consumed before the task runs, so bits raised DURING the run re-arm
126 // it for the next tick.
127 DirtyMask pending_dirty{};
128 // Background: the item budget for this run.
129 std::uint32_t budget_items = 0;
130 // OnEvent: the subscribed bits that made the task due. Multiple
131 // notifications coalesce until this invocation consumes them.
132 std::uint32_t pending_events = 0;
133};
134
137 // Dirty bits this run produced; the schedule merges them into every
138 // OnDirty task's pending mask immediately, so later-phase tasks can fire
139 // in the same tick and earlier-phase tasks fire next tick.
140 DirtyMask dirty_mask = {};
141 // Background: work units consumed (at most the offered budget).
142 std::uint32_t items_done = 0;
143 // Background: true keeps the task due next tick without a new trigger.
144 bool more_work = false;
145 // Event bits produced by this run. Later phases observe them in the same
146 // tick; earlier phases observe them on the next tick.
147 std::uint32_t event_mask = 0;
148};
149
151using ScheduleTaskFn = ScheduleTaskResult (*)(void* ctx,
152 const ScheduleTaskContext&);
153
155using ScheduleNoThrowTaskFn =
156 ScheduleTaskResult (*)(void* ctx, const ScheduleTaskContext&) noexcept;
157
160 // Static-storage label (same rule as diagnostics trace labels).
161 std::string_view name;
162 SimPhase phase = SimPhase::PreUpdate;
163 Cadence cadence{};
164};
165
168 std::uint64_t runs = 0;
169 // Ticks on which the task was due but disabled.
170 std::uint64_t skipped = 0;
171 std::uint64_t background_items = 0;
172 std::uint64_t last_run_tick = 0;
173};
174
177 std::uint64_t tick = 0;
178 std::uint32_t tasks_due = 0;
179 std::uint32_t tasks_run = 0;
180 std::uint32_t tasks_skipped = 0;
181 std::uint32_t background_items = 0;
182 // Union of every task result's dirty mask this tick.
183 DirtyMask dirty_mask_produced{};
184 // Union of every task result's event mask this tick.
185 std::uint32_t event_mask_produced = 0;
186};
187
193class Schedule {
194 public:
195 using TaskId = std::uint32_t;
196
197 // Setup-time capacity; add_task within it never reallocates, and run_tick
198 // never allocates at all.
199 void reserve_tasks(std::size_t count) {
200 if (sealed_) {
201 detail::fail_fast("Schedule::reserve_tasks called after seal()");
202 }
203 tasks_.reserve(count);
204 phase_order_.reserve(count);
205 dirty_task_ids_.reserve(count);
206 event_task_ids_.reserve(count);
207 }
208
209 auto add_task(const ScheduleTaskDesc& desc, void* ctx, ScheduleTaskFn fn)
210 -> TaskId {
211 return add_task_record(desc, ctx, fn, nullptr);
212 }
213
214 auto add_task(const ScheduleTaskDesc& desc, void* ctx,
215 ScheduleNoThrowTaskFn fn) -> TaskId {
216 return add_task_record(desc, ctx, nullptr, fn);
217 }
218
219 // Preserve the original null-callback assertion path now that two erased
220 // function-pointer overloads exist; a bare nullptr must not be ambiguous.
221 auto add_task(const ScheduleTaskDesc& desc, void* ctx, std::nullptr_t)
222 -> TaskId {
223 return add_task_record(desc, ctx, nullptr, nullptr);
224 }
225
226 // Registers a task OBJECT the caller owns; `task` must outlive the
227 // schedule. T is any callable taking the context and returning a result.
228 template <typename T>
229 auto add_task(const ScheduleTaskDesc& desc, T& task) -> TaskId {
230 if constexpr (std::is_nothrow_invocable_r_v<ScheduleTaskResult, T&,
231 const ScheduleTaskContext&>) {
232 return add_task(desc, static_cast<void*>(&task),
233 [](void* ctx, const ScheduleTaskContext& context) noexcept
235 return (*static_cast<T*>(ctx))(context);
236 });
237 } else {
238 return add_task(
239 desc, static_cast<void*>(&task),
240 [](void* ctx,
241 const ScheduleTaskContext& context) -> ScheduleTaskResult {
242 return (*static_cast<T*>(ctx))(context);
243 });
244 }
245 }
246
247 // Freezes registration and builds the dispatch indexes: phase_order_
248 // (phase-major, registration-stable -- the order run_tick always had,
249 // now one pass instead of SimPhase::Count passes over every task) and
250 // dirty/event subscription indexes. Trigger merges stop writing tasks that
251 // never read the corresponding value. Storage is never reordered, so
252 // TaskIds remain valid for the schedule's lifetime. A contract-
253 // violating add_task after seal() asserts in debug builds; under NDEBUG
254 // the late task registers but never dispatches (it is absent from the
255 // frozen indexes).
256 void seal() {
257 // Idempotent: registration is frozen after the first seal, so there is
258 // nothing to rebuild -- and a redundant seal() from inside a task
259 // callback must not rebuild phase_order_ while run_tick iterates it
260 // (Codex review of the audit3 W3 change).
261 if (sealed_) {
262 return;
263 }
264 phase_order_.clear();
265 dirty_task_ids_.clear();
266 event_task_ids_.clear();
267 for (std::uint8_t phase = 0;
268 phase < static_cast<std::uint8_t>(SimPhase::Count); ++phase) {
269 for (std::size_t i = 0; i < tasks_.size(); ++i) {
270 if (static_cast<std::uint8_t>(tasks_[i].desc.phase) == phase) {
271 phase_order_.push_back(static_cast<TaskId>(i));
272 }
273 }
274 }
275 for (std::size_t i = 0; i < tasks_.size(); ++i) {
276 if (tasks_[i].desc.cadence.kind == CadenceKind::OnDirty) {
277 dirty_task_ids_.push_back(static_cast<TaskId>(i));
278 }
279 if (tasks_[i].desc.cadence.kind == CadenceKind::OnEvent) {
280 event_task_ids_.push_back(static_cast<TaskId>(i));
281 }
282 }
283 sealed_ = true;
284 }
285
286 [[nodiscard]] auto sealed() const noexcept -> bool { return sealed_; }
287
288 // A TaskId only ever comes from add_task, so an out-of-range one is a
289 // caller bug, not a runtime condition. Asserting and then silently
290 // succeeding meant a release build accepted the bug and left the task in
291 // whatever state it already had, so the symptom surfaced later as a task
292 // that inexplicably would not turn off.
293 void set_enabled(TaskId id, bool enabled) noexcept {
294 if (id >= tasks_.size()) {
295 detail::fail_fast("Schedule::set_enabled called with an unknown TaskId");
296 }
297 tasks_[id].enabled = enabled;
298 }
299
300 // Arms the task to be due on the next run_tick regardless of cadence --
301 // the Manual trigger, and the initial trigger for Background tasks. An
302 // OnDirty task poked this way runs with pending_dirty == 0: treat a
303 // zero mask as a full-run request, not a no-op.
304 void request_run(TaskId id) noexcept {
305 if (id >= tasks_.size()) {
306 detail::fail_fast("Schedule::request_run called with an unknown TaskId");
307 }
308 tasks_[id].run_requested = true;
309 }
310
311 // Merges external dirty bits into the pending masks that can consume
312 // them (only OnDirty cadences read pending_mask; foreign bits within an
313 // OnDirty task's mask sit inert). Frame-owner thread only; never call
314 // from an op callback.
315 void notify_dirty(DirtyMask mask) noexcept {
316 if (sealed_) {
317 for (const auto id : dirty_task_ids_) {
318 tasks_[id].pending_mask |= mask;
319 }
320 return;
321 }
322 for (auto& task : tasks_) {
323 task.pending_mask |= mask;
324 }
325 }
326
327 // Coalesces external event wakeups into subscribed tasks. The event mask
328 // is only a deterministic scheduler trigger; applications retain exact
329 // payloads and tick ordering in EventStream<T>.
330 void notify_events(std::uint32_t mask) noexcept {
331 if (sealed_) {
332 for (const auto id : event_task_ids_) {
333 tasks_[id].pending_events |= mask;
334 }
335 return;
336 }
337 for (auto& task : tasks_) {
338 task.pending_events |= mask;
339 }
340 }
341
342 // Publishes an exact payload before arming its coalesced scheduler mask.
343 // A full stream rejects the payload and deliberately does not wake tasks.
344 template <typename T>
345 [[nodiscard]] bool publish_event(std::uint32_t mask, EventStream<T>& stream,
346 std::uint64_t tick, const T& value) {
347 if (!stream.publish(tick, value)) {
348 return false;
349 }
350 notify_events(mask);
351 return true;
352 }
353
354 auto run_tick(SimClock& clock) -> ScheduleTickStats {
355#if TESS_DIAGNOSTICS_ENABLED
356 diagnostics::ScopedTimer tick_timer{diagnostics::TraceCategory::Scheduler,
357 "schedule_tick"};
358#endif
359 if (!sealed_) {
360 detail::fail_fast("Schedule::run_tick called before seal()");
361 }
362 if (in_run_) {
363 detail::fail_fast("Schedule::run_tick rejected a reentrant run_tick");
364 }
365 // Scope guard rather than a trailing store: a throwing task callback
366 // must not leave the schedule latched "in run", or every subsequent
367 // tick would fail the reentrancy contract.
368 struct InRunGuard {
369 bool& flag;
370 ~InRunGuard() { flag = false; }
371 };
372 in_run_ = true;
373 const InRunGuard guard{in_run_};
374 auto stats = ScheduleTickStats{};
375 stats.tick = advance_sim_tick(clock);
376
377 for (std::size_t position = 0; position < phase_order_.size(); ++position) {
378#if TESS_HAS_EXCEPTIONS
379 try {
380 run_task_if_due(tasks_[phase_order_[position]], clock, stats);
381 } catch (...) {
382 // The fixed tick happened even though its remaining callbacks did
383 // not. Advance their EveryN counters without consuming manual or
384 // event triggers so tasks on opposite sides of the thrower keep the
385 // same cadence phase on later ticks.
386 for (++position; position < phase_order_.size(); ++position) {
387 advance_aborted_tick_cadence(tasks_[phase_order_[position]]);
388 }
389 throw;
390 }
391#else
392 run_task_if_due(tasks_[phase_order_[position]], clock, stats);
393#endif
394 }
395 return stats;
396 }
397
398 // Same precondition as set_enabled, and the silent fallback was worse
399 // here: a default-constructed ScheduleTaskStats is all zeroes, which is
400 // exactly what a real, registered task that has never run reports. The
401 // caller could not tell "you passed a bad id" from "this task is idle".
402 [[nodiscard]] auto task_stats(TaskId id) const noexcept -> ScheduleTaskStats {
403 if (id >= tasks_.size()) {
404 detail::fail_fast("Schedule::task_stats called with an unknown TaskId");
405 }
406 return tasks_[id].stats;
407 }
408
409 [[nodiscard]] auto task_count() const noexcept -> std::size_t {
410 return tasks_.size();
411 }
412
413 private:
414 struct TaskRecord {
415 ScheduleTaskDesc desc{};
416 void* ctx = nullptr;
417 ScheduleTaskFn fn = nullptr;
418 ScheduleNoThrowTaskFn no_throw_fn = nullptr;
419 DirtyMask pending_mask{};
420 std::uint32_t pending_events = 0;
421 std::uint32_t ticks_until_due = 0;
422 bool run_requested = false;
423 bool in_progress = false;
424 bool enabled = true;
425 ScheduleTaskStats stats{};
426 };
427
428 auto add_task_record(const ScheduleTaskDesc& desc, void* ctx,
429 ScheduleTaskFn fn, ScheduleNoThrowTaskFn no_throw_fn)
430 -> TaskId {
431 if (sealed_) {
432 detail::fail_fast("Schedule::add_task called after seal()");
433 }
434 if (fn == nullptr && no_throw_fn == nullptr) {
435 detail::fail_fast("Schedule::add_task received a null callback");
436 }
437 if (static_cast<std::uint8_t>(desc.phase) >=
438 static_cast<std::uint8_t>(SimPhase::Count)) {
439 detail::fail_fast("Schedule::add_task received an invalid SimPhase");
440 }
441 if (static_cast<std::uint8_t>(desc.cadence.kind) >
442 static_cast<std::uint8_t>(CadenceKind::Manual)) {
443 detail::fail_fast("Schedule::add_task received an invalid CadenceKind");
444 }
445 if (desc.cadence.kind == CadenceKind::EveryN && desc.cadence.every_n == 0) {
446 detail::fail_fast(
447 "Schedule::add_task EveryN cadence requires every_n > 0; use "
448 "Cadence::every_ticks() to normalize input");
449 }
450 if (desc.cadence.kind == CadenceKind::Background &&
451 desc.cadence.budget.max_items == 0) {
452 detail::fail_fast(
453 "Schedule::add_task Background cadence requires a nonzero budget; "
454 "use Cadence::background() to normalize input");
455 }
456 auto record = TaskRecord{};
457 record.desc = desc;
458 record.ctx = ctx;
459 record.fn = fn;
460 record.no_throw_fn = no_throw_fn;
461 if (record.desc.cadence.every_n == 0) {
462 record.desc.cadence.every_n = 1;
463 }
464 if (record.desc.cadence.budget.max_items == 0) {
465 record.desc.cadence.budget.max_items = 1;
466 }
467 if (desc.cadence.kind == CadenceKind::EveryN) {
468 record.ticks_until_due = record.desc.cadence.every_n;
469 }
470 tasks_.push_back(record);
471 return static_cast<TaskId>(tasks_.size() - 1);
472 }
473
474 static void advance_aborted_tick_cadence(TaskRecord& task) noexcept {
475 if (task.desc.cadence.kind != CadenceKind::EveryN) {
476 return;
477 }
478 if (--task.ticks_until_due == 0) {
479 task.ticks_until_due = task.desc.cadence.every_n;
480 }
481 }
482
483 void run_task_if_due(TaskRecord& task, SimClock clock,
484 ScheduleTickStats& stats) {
485 // Cadence bookkeeping advances even while a task is disabled, so
486 // re-enabling never shifts the lockstep phase of EveryN tasks;
487 // OnDirty/Manual/Background triggers PERSIST across disablement and
488 // fire on the first enabled tick.
489 auto due = false;
490 auto fired_dirty = DirtyMask{};
491 auto fired_events = std::uint32_t{0};
492 auto budget = std::uint32_t{0};
493 switch (task.desc.cadence.kind) {
494 case CadenceKind::EveryTick:
495 due = true;
496 break;
497 case CadenceKind::EveryN: {
498 // The countdown advances independently of manual pokes, so a
499 // request_run never shifts the lockstep phase -- it just adds one
500 // extra run.
501 const auto counted = --task.ticks_until_due == 0;
502 if (counted) {
503 task.ticks_until_due = task.desc.cadence.every_n;
504 }
505 due = counted || task.run_requested;
506 break;
507 }
508 case CadenceKind::OnDirty:
509 fired_dirty = task.pending_mask & task.desc.cadence.dirty_mask;
510 due = static_cast<bool>(fired_dirty) || task.run_requested;
511 break;
512 case CadenceKind::OnEvent:
513 fired_events = task.pending_events & task.desc.cadence.event_mask;
514 due = fired_events != 0 || task.run_requested;
515 break;
516 case CadenceKind::Background:
517 due = task.in_progress || task.run_requested;
518 budget = task.desc.cadence.budget.max_items;
519 break;
520 case CadenceKind::Manual:
521 due = task.run_requested;
522 break;
523 }
524 if (!due) {
525 return;
526 }
527 ++stats.tasks_due;
528 if (!task.enabled) {
529 // EveryN consumed its countdown above (already reset); persistent
530 // triggers stay armed for the first enabled tick.
531 ++stats.tasks_skipped;
532 ++task.stats.skipped;
533 return;
534 }
535
536 // Consume triggers BEFORE invoking, so anything raised during the run
537 // re-arms the task for the next tick instead of being lost.
538 task.pending_mask &= ~fired_dirty;
539 task.pending_events &= ~fired_events;
540#if TESS_HAS_EXCEPTIONS
541 const auto consumed_request = task.run_requested;
542#endif
543 task.run_requested = false;
544
545 auto context = ScheduleTaskContext{};
546 context.clock = clock;
547 context.pending_dirty = fired_dirty;
548 context.budget_items = budget;
549 context.pending_events = fired_events;
550 auto result = ScheduleTaskResult{};
551#if TESS_DIAGNOSTICS_ENABLED
552 diagnostics::ScopedTimer task_timer{diagnostics::TraceCategory::Scheduler,
553 task.desc.name};
554#endif
555#if TESS_HAS_EXCEPTIONS
556 if (task.no_throw_fn != nullptr) {
557 result = task.no_throw_fn(task.ctx, context);
558 } else {
559 try {
560 result = task.fn(task.ctx, context);
561 } catch (...) {
562 // A failed callback did not complete the work represented by its
563 // coalesced triggers. Merge rather than assign: the callback may have
564 // raised the same or additional triggers before it threw.
565 task.pending_mask |= fired_dirty;
566 task.pending_events |= fired_events;
567 task.run_requested = task.run_requested || consumed_request;
568 throw;
569 }
570 }
571#else
572 if (task.no_throw_fn != nullptr) {
573 result = task.no_throw_fn(task.ctx, context);
574 } else {
575 result = task.fn(task.ctx, context);
576 }
577#endif
578 if (task.desc.cadence.kind == CadenceKind::Background &&
579 result.items_done > budget) {
580 detail::fail_fast(
581 "Schedule task reported more background items than offered");
582 }
583 if (task.desc.cadence.kind != CadenceKind::Background &&
584 result.items_done != 0) {
585 detail::fail_fast(
586 "Schedule non-background task reported background items");
587 }
588
589 task.in_progress =
590 task.desc.cadence.kind == CadenceKind::Background && result.more_work;
591 if (result.dirty_mask) {
592 // Immediate merge: later-phase OnDirty tasks see it this tick,
593 // earlier-phase (and this) tasks next tick. Only OnDirty tasks
594 // consume pending_mask, so only they receive it.
595 for (const auto id : dirty_task_ids_) {
596 tasks_[id].pending_mask |= result.dirty_mask;
597 }
598 stats.dirty_mask_produced |= result.dirty_mask;
599 }
600 if (result.event_mask != 0) {
601 for (const auto id : event_task_ids_) {
602 tasks_[id].pending_events |= result.event_mask;
603 }
604 stats.event_mask_produced |= result.event_mask;
605 }
606
607 ++stats.tasks_run;
608 stats.background_items += result.items_done;
609 ++task.stats.runs;
610 task.stats.background_items += result.items_done;
611 task.stats.last_run_tick = clock.tick;
612 }
613
614 std::vector<TaskRecord> tasks_;
615 // Built at seal(): phase-major dispatch order and trigger subscribers.
616 std::vector<TaskId> phase_order_;
617 std::vector<TaskId> dirty_task_ids_;
618 std::vector<TaskId> event_task_ids_;
619 bool sealed_ = false;
620 bool in_run_ = false;
621};
622
623// Frame -> ticks bridge: consumes real frame time through the accumulator
624// (honoring SimSpeed and the per-frame tick cap) and runs the schedule once
625// per granted fixed tick. Cadences therefore count FIXED TICKS, never
626// frames: an EveryN task at 2x speed fires twice as often in real time and
627// exactly as often in sim time, and a backlogged frame that grants several
628// ticks advances every cadence through each of them.
631 std::size_t ticks = 0;
632 double alpha = 0.0;
633 double dropped_seconds = 0.0;
634 // Stats of the LAST tick this frame (zero ticks leaves it default).
635 ScheduleTickStats last_tick{};
636};
637
639inline auto run_schedule_frame(Schedule& schedule, SimClock& clock,
640 FixedStepAccumulator& accumulator,
641 double real_delta_seconds,
643 const auto frame = accumulator.consume(real_delta_seconds, control);
644 auto summary = ScheduleFrameSummary{};
645 summary.ticks = frame.ticks;
646 summary.alpha = frame.alpha;
647 summary.dropped_seconds = frame.dropped_seconds;
648 for (std::size_t i = 0; i < frame.ticks; ++i) {
649 summary.last_tick = schedule.run_tick(clock);
650 }
651 return summary;
652}
653
654} // namespace tess
Definition event_stream.h:33
Definition time.h:51
Definition schedule.h:193
Definition trace.h:290
Bounds background work in deterministic item units per task invocation.
Definition schedule.h:55
Configures when a task becomes due within the fixed-tick schedule.
Definition schedule.h:60
Definition metadata_types.h:12
Summarizes all fixed ticks consumed during one rendered frame.
Definition schedule.h:630
Supplies a task with the current tick, trigger bits, and work allowance.
Definition schedule.h:122
Describes a task's static label, phase, and cadence.
Definition schedule.h:159
Returns produced dirty bits and bounded background progress to the schedule.
Definition schedule.h:136
Holds cumulative execution counters for one task.
Definition schedule.h:167
Summarizes task dispatch and dirty propagation for one fixed tick.
Definition schedule.h:176
Stores the authoritative monotonically increasing fixed-tick count.
Definition time.h:28
Supplies the time-control state consumed for one rendered frame.
Definition time.h:20