Bevy 0.17 ECS Plugin Schedule Order Debug for Fest Demo Menu Hitches - 2026
Your fest demo menu stutters for three frames when you open settings. Profiling shows no obvious GPU spike. Sprite count is modest. Someone on Discord says “just batch your sprites” — but the hitch is gone if you move one add_systems line above another.
2026 H2 is the window where Bevy 0.17 ships in public fest builds: required components and observers rewrote tutorials, but schedule order is still where micro-studios lose afternoons. Menu hitches are often ordering bugs: input vs state vs UI layout vs deferred World mutations applied in the wrong stage.
This Programming & Technical playbook is the schedule and plugin order layer. It pairs with the beginner weekend build in Your First Bevy 0.17 2D Platformer in One Weekend (API walk-through) and the guide chapter on Bevy 0.17 diagnostics, Tracy, and frame-time budget (profiling receipts). This URL owns why the menu hitches when order is wrong — not another sprite tutorial.
Non-repetition note: The weekend tutorial teaches what to type. This post teaches where in the schedule it runs and how to prove order with bevy_schedule_receipt_v1.json before you freeze a fest branch.
Why this matters now (2026 H2)
- October Next Fest and itch capsule traffic punish one-frame UI pops — players read them as “janky port,” not as “we had a deferred command ordering surprise.”
- Bevy 0.17’s plugin graph is larger —
DefaultPluginsplus UI, audio, and your gameplay plugins interact in PreUpdate / Update / PostUpdate lanes. Mis-ordered systems look like performance problems. - Observers and required components moved code off hot loops — the remaining hitch classes cluster around state transitions and command flushes.
- Receipt culture — partners and future-you want a
build_labeland a schedule receipt, not a screen recording alone. Pair with BUILD_RECEIPT JSON evening pipeline when you wire optionalbevy_schedule_order_okbooleans. - Steamworks evidence rows — when you ship a PC demo, store QA lists expect reproducible build notes; cross-link 18 Free Steamworks and PC Distribution Integration Resources for upload log discipline beside engine order.
Direct answer: Treat menu hitch debug as schedule graph work: list plugins, list systems per stage, enforce SystemSet chains for menu open/close, verify deferred flushes, then file bevy_schedule_receipt_v1.json before fest promotion. If you cannot draw that graph on a whiteboard in five minutes, you are not done triaging yet.
Who this is for
| Audience | You get |
|---|---|
| Beginner who finished the weekend tutorial | A checklist to read schedule order before blaming art |
| Working Rust dev | SystemSet patterns, deferred-command traps, Tracy pairing |
| Solo maintainer | A receipt JSON you can attach to release-evidence/ |
Time: ~45 minutes to read; 90–120 minutes for first receipt pass on a real repo.
Beginner path — the four questions
Before opening Tracy, answer these in order:
- Which schedule stage owns the hitch? (Startup is different from every-frame
Update.) - Does opening the menu spawn/despawn entities with
Commands? If yes, you care about flush points. - Does any system read UI state in the same stage another system writes it? That is a plain ordering bug.
- Did a plugin register systems implicitly? Third-party plugins often add systems you forgot to name in stand-up.
If all four are “unknown,” stop profiling draw calls — you do not have a measurement problem yet.
Developer path — the ordering model
Plugins register systems in registration order
App::add_plugins runs plugin build functions in the order you pass them. When two plugins both insert systems into Update, registration order tends to preserve relative order within that set — until you reorder plugins or use explicit sets.
Fest rule: Put DefaultPlugins first unless you have a documented exception. Then add domain plugins in a stable order: input smoothing, game state, UI shell, gameplay. Changing that order between release candidates without a receipt is how “random” menu hitches return.
Stages are buckets — misuse buckets and you get hitches
Common layout:
- PreUpdate — input aggregation, low-latency reads.
- Update — gameplay simulation, AI, physics stepping decisions.
- PostUpdate — transforms, camera follow, often UI layout reactions.
Menu hitches often trace to running gameplay cleanup in Update while UI still reads old hierarchy in the same stage — split them with sets or move UI reactions to PostUpdate (or an explicit set after your state transition writers).
SystemSet is your contract language
Instead of “system A before system B” comments, encode:
configure_sets(Update, (MenuSystems::Input, MenuSystems::ApplyState, MenuSystems::RebuildUi).chain())
Name sets like production API: MenuSystems::ApplyState beats Set1.
Deferred commands and one-frame ghosts
When you open a menu you often commands.spawn panels and despawn old widgets. Observers and required components can amplify surprise if teardown runs in the same stage as code that still queries the old graph.
Pattern: State transition writers enqueue commands; UI readers run after the flush boundary you choose — either next stage or an ordered set after apply_state_transition analog in your codebase.
Fixed timestep vs variable UI
If you tie menu animation to FixedUpdate but read input in Update, you can see micro-stutter when fixed accumulators catch up. Fest demos often enable fixed timestep for platforming; keep menu feel on Update unless you have a deliberate reason not to.
Symptom → likely cause map
| Symptom | Likely cause | First move |
|---|---|---|
| Hitch only when opening/closing menu | Order vs Commands flush |
Chain sets; split stages |
| Hitch every N frames with stable N | Fixed timestep + UI mismatch | Move UI out of FixedUpdate |
| Hitch after adding a plugin | Implicit systems in new plugin | Dump plugin order; name systems |
| Hitch only in release | Different feature flags / tracing off | Compare schedules; enable Tracy |
| Hitch on WASM only | Main thread + browser paint | Pair wasm boot preflight + frame budget |
Gates B1–B6 (promotion discipline)
Use this table before you call the fest branch “menu stable.”
| Gate | Check |
|---|---|
| B1 | Every menu-related system is in a named SystemSet |
| B2 | Update menu sets are chain()-ordered or explicitly ordered with before/after |
| B3 | No system reads UI hierarchy in the same set that despawns those nodes without documented flush |
| B4 | Plugin registration order is frozen in CHANGELOG for the fest candidate |
| B5 | Tracy capture shows hitch within a named span — not “unknown bucket” |
| B6 | bevy_schedule_receipt_v1.json committed under release-evidence/ |
bevy_schedule_receipt_v1.json (template)
{
"schema": "bevy_schedule_receipt_v1",
"build_label": "fest-demo-2026-10-rc3",
"bevy_version": "0.17",
"plugin_registration_order": [
"DefaultPlugins",
"GameInputPlugin",
"GameStatePlugin",
"MenuShellPlugin",
"GameplayPlugin"
],
"menu_pipeline": {
"stage": "Update",
"system_sets": [
"MenuInputSet",
"MenuStateTransitionSet",
"MenuUiRebuildSet"
],
"chain_order": [
"MenuInputSet",
"MenuStateTransitionSet",
"MenuUiRebuildSet"
],
"uses_commands_in_transition": true,
"deferred_flush_strategy": "same_stage_ordered_after_transition"
},
"tracy": {
"enabled": true,
"capture_seconds": 90,
"notes": "Hitch spans named MenuOpen / MenuRebuild"
},
"gates": {
"B1_named_sets": true,
"B2_chain_or_explicit_order": true,
"B3_no_same_set_read_write_hazard": true,
"B4_plugin_order_frozen": true,
"B5_tracy_named_span": true,
"B6_receipt_committed": true
},
"promotion_allowed": true
}
Honest limit: this JSON does not replace cargo tree or your CI — it is a human contract for what you believe the schedule is.
Tracy pairing (working dev)
Follow the guide chapter for trace_tracy, FrameTimeDiagnosticsPlugin, and the 2D frame budget table:
Bevy 0.17 Diagnostics, Tracy, and Frame-Time Budget Preflight for 2D Ship Candidates
For menu hitches specifically:
- Name spans around open, close, and rebuild paths — not only
Updateroot. - Capture 90 seconds including three open/close cycles.
- If the hitch is shorter than one frame in Tracy but visible on screen, suspect GPU sync / vsync — still log the engine-side span first so you do not optimize the wrong layer.
Course and help crosswalk
- Course milestone pairing: Lesson 244 — Q3 summer playtest capstone receipt expects evidence culture across tools — use
bevy_schedule_receipt_v1.jsonas a cousin artifact when Bevy is your fest engine. - Guide preflight: Bevy 0.17 playtest crash log and build_label correlation pairs label truth with crash evidence — schedule receipts belong in the same
release-evidence/tag when triaging “menu crashed after build X.” - Help fixes: Bevy 0.17 asset hot reload silent PNG watcher and required-components panic migration are adjacent lanes — hot reload and component panic noise masquerade as “menu hitch” if you reload assets while UI rebuilds.
Wednesday smoke pairing
Slot a 60-second menu open/close loop into Wednesday demo build smoke ritual. If smoke passes but Tracy still shows a spike only on first open, treat that as cold-cache or shader first compile — document it in the receipt notes field instead of silently accepting regressions.
Evidence folder layout
Under release-evidence taxonomy, place schedule artifacts in:
release-evidence/marketing-and-demo/bevy-schedule-2026-10/
bevy_schedule_receipt_v1.json
tracy_menu_open_close_pr90.tcap
plugin_order_changelog_snip.md
If your studio keeps engine evidence under build-and-binary/, mirror the same three files there — consistency beats perfect folder theology.
Anti-patterns (reject in code review)
- “Just
add_systems(Update, …)in whatever order we typed them.” - State transition systems that also rebuild UI in the same anonymous tuple.
- Plugin A registers a system that must run after plugin B’s systems, but A is added before B.
- Menu logic in
FixedUpdatewithout documenting why. - Removing Tracy spans because the capture “looks noisy.”
- Promoting a fest build when B3 is false — you are shipping a latent one-frame UI bug.
Optional automation sketch
If you use CI, a lightweight guard is to fail when bevy_schedule_receipt_v1.json is missing for tags matching fest-*:
test -f release-evidence/marketing-and-demo/bevy-schedule-2026-10/bevy_schedule_receipt_v1.json
Pair with jq checks on promotion_allowed if you want a hard gate — only after humans trust the JSON schema.
Annotated configure_sets sketch (copy shape, not prose)
The following is illustrative Rust shape — names and imports will vary with your crate layout. The point is to show three named sets in Update that always run in order when the menu is active:
// Illustrative only — adjust to your State type and system signatures.
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
enum MenuSchedule {
ReadInput,
TransitionGameState,
RebuildUi,
}
fn plugin(app: &mut App) {
app.configure_sets(
Update,
(
MenuSchedule::ReadInput,
MenuSchedule::TransitionGameState,
MenuSchedule::RebuildUi,
)
.chain(),
)
.add_systems(
Update,
(
read_menu_input.in_set(MenuSchedule::ReadInput),
apply_menu_state_change.in_set(MenuSchedule::TransitionGameState),
rebuild_menu_ui.in_set(MenuSchedule::RebuildUi),
),
);
}
Why three sets instead of one tuple? Because your producer can read the receipt: “RebuildUi is last.” A single tuple of six anonymous systems does not compress into stand-up language.
run_if guards (keep sets from firing in gameplay)
Menu systems should not compete with exploration systems. Typical pattern:
run_if(in_state(MenuState::Open))on rebuild systemsrun_if(in_state(GameState::Playing))on gameplay writers that must not touch menu entities
If you forget run_if, you get “phantom” systems that still run and still allocate queries — Tracy looks busy even when the screen is idle.
Exclusive systems (when you truly need a hard barrier)
Sometimes you need an exclusive system to inspect the World directly for one frame (debug tooling, one-off migration, emergency assert). Exclusive systems block parallelization for that system’s slot — fine for dev, dangerous if left in fest hot paths.
Fest rule: no exclusive systems in Update for menu unless documented in the receipt with why and measured cost.
One-day debug workflow (repeatable)
| Hour block | Task | Output |
|---|---|---|
| 0:00–0:20 | List plugins in main.rs order |
plugin_registration_order in receipt |
| 0:20–0:45 | Export Tracy 90s with menu spam | .tcap + screenshot of worst spike |
| 0:45–1:10 | Name sets + chain() for menu |
PR with only ordering change |
| 1:10–1:30 | Re-capture Tracy | Compare p95 before/after |
| 1:30–2:00 | Fill bevy_schedule_receipt_v1.json + link in BUILD_RECEIPT optional row |
Evidence path in repo |
If the second Tracy capture shows no named span where the hitch is felt, you are still measuring the wrong lane — go back to B5.
“Feels like Unity script order” — what is actually the same
Indies coming from Unity often think in Script Execution Order. Bevy’s answer is explicit scheduling instead of hidden inspector integers.
| Unity habit | Bevy analogue |
|---|---|
Default Awake/Start ordering anxiety |
Startup systems + plugin build order |
Update vs LateUpdate |
Update vs PostUpdate + your sets |
OnEnable UI rebuild |
Same-frame Commands + ordered sets |
| Deep profiler timeline | Tracy spans + diagnostics plugins |
The emotional shift: order is not accidental — you encode it or you inherit whatever registration order your dependencies chose.
HDR store capture sibling (format ladder)
If your fest marketing thread is also fighting blown highlights in PNGs, that is the art lane — not this schedule lane:
HDR Screenshot False Bloom When Downscaling to Steam Store Captures
Keep engine receipts and marketing receipts in different JSON files so Thursday row review stays legible.
Common Bevy 0.17 menu stacks (names to grep)
When you inherit a jam game, grep for these plugin/system names — each often injects implicit order constraints:
bevy_ui— layout and focus interact with transform propagation timing.bevy_state—OnEnter/OnExitschedules are easy to mis-pair with UI rebuild.bevy_input/bevy_window— low-latency input vs winit events can reorder relative to your assumptions on Linux vs Windows.bevy_audio/bevy_sprite— not “menu,” but they still stealUpdatetime and change profiling baselines.
Document which crates are in the minimal fest feature set vs dev-only tooling (bevy_inspector_egui, editor overlays). A fest receipt should list features enabled on the candidate binary, not only bevy semver.
FAQ
Do I need a custom schedule label?
Not on day one. Named SystemSets in Update plus explicit ordering often fix fest menu hitches.
Is this the same as “use observers instead of systems”?
No. Observers reduce polling, but ordering still matters when observers enqueue Commands that interact with UI spawned in the same frame.
What if the hitch disappears in --release?
Compare feature flags and plugin lists. Some diagnostics plugins change timing. Your receipt should record debug vs release schedule assumptions separately if they diverge.
Should WASM demos use the same receipt?
Yes — add a target field (wasm32-unknown-unknown) and note main-thread constraints. Pair WASM memory ceiling playbook mindset even when the engine is Bevy: the store page is still a browser surface.
Should I split menus into a separate SubApp?
Usually not for a first fest slice. A second SubApp adds scheduler mental overhead. Prefer named SystemSets and strict run_if guards until Tracy proves the main-world schedule is the bottleneck. If you do split later, duplicate the receipt pattern: one JSON section per App so reviewers know which scheduler owns menu rebuild.
Key takeaways
- Fest menu hitches in Bevy 0.17 are often schedule order bugs, not sprite batching.
- Plugin registration order and stage choice are part of your public API — freeze them.
SystemSet+chain()is the cheapest contract language for menu pipelines.Commands+ same-stage readers cause one-frame ghosts — split with order or stages.bevy_schedule_receipt_v1.jsonturns tribal knowledge into promotion evidence.- Gates B1–B6 give producers a binary pass/fail without reading Rust.
- Pair Tracy from the diagnostics guide chapter for named spans, not only FPS overlays.
- Wednesday smoke should include a deliberate menu loop, not only gameplay movement.
- Help hot reload and required-component fixes are adjacent — grep those when “menu hitch” is reported after asset saves.
- Steamworks resource lists belong in the same evidence story as engine receipts for PC fest demos.
Related reading
- Your First Bevy 0.17 2D Platformer in One Weekend
- Bevy 0.17 diagnostics, Tracy, and frame-time budget preflight
- Bevy 0.17 required components, observers, and asset hot reload
- BUILD_RECEIPT JSON evening pipeline
- Wednesday demo build smoke ritual
- 18 Free Steamworks and PC Distribution Integration Resources
- Lesson 244 — Q3 summer playtest capstone receipt
Outbound:
- Bevy API docs on docs.rs (pin to your
Cargo.lockminor; schedule APIs move between minors).
Next in cluster: blog backlog #8 October fest week founder time-box worksheet.