Skyline plugin for Super Smash Bros. Ultimate 13.0.5: stops the task dispatcher busy-polling empty queues while it waits (fixes the character-select slowdown)
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
crashbandicode 4471e05274 Build as a cdylib only so the release profile's LTO applies
Cargo skips LTO for a library that is also an rlib, so `lto = "thin"`
never applied. The unit tests do not need the rlib.

Release NRO resident size (.text + .rodata + .data + .bss) 516 -> 72
KiB; exported symbols unchanged. Tests (7), clippy --all-targets, the
no-default-features build and the Skyline build and clippy pass. Not run
in Eden or on the Switch.
2026-10-30 22:58:36 -04:00
docs Version 0.1.1: install log names the plugin version; Switch record and both-forge releases in the docs 2026-10-19 23:26:30 -04:00
src Version 0.1.1: install log names the plugin version; Switch record and both-forge releases in the docs 2026-10-19 23:26:30 -04:00
.gitignore Dispatcher idle backoff plugin for Smash Ultimate 13.0.5 2026-10-12 15:31:03 -04:00
Cargo.lock Version 0.1.1: install log names the plugin version; Switch record and both-forge releases in the docs 2026-10-19 23:26:30 -04:00
Cargo.toml Build as a cdylib only so the release profile's LTO applies 2026-10-30 22:58:36 -04:00
LICENSE Dispatcher idle backoff plugin for Smash Ultimate 13.0.5 2026-10-12 15:31:03 -04:00
README.md Version 0.1.1: install log names the plugin version; Switch record and both-forge releases in the docs 2026-10-19 23:26:30 -04:00
rust-toolchain.toml Dispatcher idle backoff plugin for Smash Ultimate 13.0.5 2026-10-12 15:31:03 -04:00

ssbu-dispatcher-backoff

Disclaimer: this project was developed with the assistance of AI (Claude, Codex and other coding agents) under the operator's direction and review. Every result recorded here comes from the dated tests cited below; nothing is claimed beyond those records.

A small Skyline plugin for Super Smash Bros. Ultimate 13.0.5 that stops the game's task dispatcher from busy-polling six empty job queues while it waits for an unfinished dependency. The waste is in the game's own code and is largest on the character select screen, where the main thread spends roughly 5 ms of every frame spinning while one UI layout animation job runs on a worker thread.

Before (Eden, 175% CPU quota, LessLag CSS) After (same boot, same scene, same quota)
before after
52–54 fps, 20.0 ms frame, 27.2 CPU ms/frame 59 fps, 16.7 ms frame, 23.3 CPU ms/frame

Those two frames come from one emulator session with the diagnostic form of the fix toggled between captures. On a Switch, the same hook built into ssbu-online-deluxe-rewrite 0.3.0 took the idle character select from 48–50 to 58–60 fps on 2026-10-12 (see Evidence). The full write-up, with call chain, numbers and the hook contract, is in docs/ANALYSIS.md.

What it does

The dispatcher (main+0x354a6d0) scans its pending-dependency records and then polls three local and three shared priority queues, each behind a std::mutex. When every queue is empty and at least one record is still pending, it goes straight back to the scan instead of waiting on its light event; completion of the awaited job never signals that event. On a loaded machine the main thread therefore spends its wait spinning through six lock/unlock pairs per pass, and a two-image presentation ring cannot hide the resulting frame-time variance.

This plugin installs one inline hook at main+0x354ab48, the instruction the dispatcher reaches after the sixth probe found no job and released its lock. The hook reads what the game is about to read anyway and sleeps the calling thread for a few microseconds when all of the following hold:

  • the calling thread is the SDK MainThread;
  • the dispatcher is enabled (context byte +0x58, bit 0);
  • all six queue sizes are zero;
  • the pending vector [d+0xe0, d+0xe8) is non-empty and every record's 32-bit dependency counter is still above zero;
  • the ready-to-resume vector [d+0xf8, d+0x100) is empty.

Anything else falls through to the game's own rescan immediately. No job, counter, lock, priority or ordering is changed; the plugin only decides whether to wait before the game repeats a scan it would have repeated anyway. The hook is integer-only and performs no allocation, file or log access.

Default cadence: the first 16 idle passes spin exactly as the game does, then the thread sleeps 5 µs, doubling to a 50 µs cap, on every idle pass until work appears. Any sign of work resets the streak.

Install

Releases are published on Forgejo and mirrored on GitHub with the same assets: the NRO, an SD-card zip, manifest.json and SHA256SUMS.txt.

Easiest: extract ssbu-dispatcher-backoff-vX.Y.Z-sd.zip to the root of the SD card. It contains only the plugin at its Skyline path and a short README; no game data.

Manual: copy libssbu_dispatcher_backoff.nro to

sd:/atmosphere/contents/01006A800016E000/romfs/skyline/plugins/

On the next boot the plugin writes

sd:/ultimate/ssbu_dispatcher_backoff/install.log

with the plugin version, the detected game version, the policy in effect, the five instruction words it found at the hook site and the result: installed, unsupported_version, skipped_site_words_differ or skipped_site_owned_by_another_plugin.

With ssbu-online-deluxe-rewrite

The same fix is built into ssbu-online-deluxe-rewrite 0.3.0 and later, on by default (its dispatcher_backoff=false switch turns it off). Both plugins verify the original bytes before patching, so whichever loads first owns the site and the other one skips: this plugin logs skipped_site_owned_by_another_plugin, Deluxe shows External in its Backoff row. Installing both is harmless but pointless. Deluxe's copy also carries an eligibility gate (no sleep before one of its scene hooks has fired or while its tracked window has not presented for 250 ms); this plugin has no other hooks to gate on and sleeps on any eligible idle pass from the moment it installs.

Tuning

Optional. Create sd:/ultimate/ssbu_dispatcher_backoff/config.ini:

# idle passes to spin before the first sleep (default 16)
spin_passes=16
# after that, sleep on every Nth idle pass (default 1 = every pass)
every_passes=1
# first sleep and the cap of the doubling ramp, in nanoseconds (1000..2000000)
base_ns=5000
max_ns=50000
# minimum gap between the end of one sleep and the next, microseconds (0 = none)
cooldown_us=0
# 1 lets worker-thread dispatchers sleep too (default 0: MainThread only)
all_threads=0
# preset=measured reproduces the diagnostic cadence that produced the numbers
# above: a 1 us request about once per 171 idle passes, from the first pass.
# preset=default restores the defaults. Later keys override a preset.

max_ns=0 disables sleeping while keeping the hook installed. Values outside the documented ranges are clamped; unknown keys are ignored.

Evidence

On the Switch

The hook has been on the operator's Switch since 2026-10-12 as part of ssbu-online-deluxe-rewrite, which installs it at boot in every build from 0.3.0 on. The operator's hardware result for the 0.3.0 build, two-image LessLag profile, default policy: character select idle 58–60 fps, up from 48–50; a live match stayed at 60. Moving the cursor hand still dropped the character select to 46 fps, with the default ramp and with preset=measured alike; the hardware captures that followed traced that drop to the two-image presentation path (a texture-ready wait after the swapchain acquire, with the game presenting a frame only after the next frame's CPU update), not to this hook, and Deluxe 0.4.0 removed it on the console by presenting at the flush (49.9 → 59.85 fps on the hand sweep). The records are Deluxe's docs/VALIDATION.md (0.3.0, 0.3.1 and 0.4.0 entries) and docs/RENDERING.md; docs/ANALYSIS.md section 6b summarizes them against the test plan.

This standalone NRO has not itself been booted on a Switch or in Eden. It is the same hook, site fingerprint and decision logic with its own install log and config reader; the test plan for it is section 6 of docs/ANALYSIS.md, and its result will be recorded there.

On the emulator

Eden (Linux, Vulkan, 1x, isolated profile) with the emulator's own systemd unit limited to 175% CPU to reproduce the Switch-like pressure. A/B/A means control, backoff, control on one boot of one binary.

Scene / profile / quota FPS A / B / A CPU ms per frame A / B / A
Character select, LessLag, 175% 52.23 / 59.31 / 51.89 27.84 / 22.90 / 28.08
Character select, Vanilla, 175% 59.32 / 60.10 / 59.85 26.90 / 23.04 / 26.27
Character select, LLDoubles, 175% 59.55 / 60.03 / 58.33 26.44 / 23.13 / 27.84
Character select, LessLag, unlimited 59.08 / 60.00 / 59.80 24.82 / 23.79 / 24.88
Live match, LessLag, 175% 57.43 / 59.35 / 59.90 20.62 / 18.74 / 19.32

Main-core CPU fell from 11.7 to 6.7 ms per frame while the UI worker stayed at 5.7–5.8 ms: the saving is removed polling, not removed work. The production hook with the default policy was then re-measured the same way: character select 55.6 → 60.0 → 54.2 fps (off / on / off) at 26.6 → 19.5 → 26.8 CPU ms per frame, Vanilla 60 with the same CPU saving, live match 59.8 fps at 16.3 CPU ms per frame. These are emulator measurements; CPU quota competition and JIT lock cost are emulator artifacts, so the numbers do not transfer to the Switch as-is. The Switch numbers above come from Deluxe's build of the hook.

Safety

  • Version gate: the plugin only acts on display version 13.0.5 and only when the five original words at the site match.
  • Scheduling only: the hook never skips or rewrites game state. That preserves the dispatcher's semantics but is not a proof that different timing cannot expose a pre-existing race, so linked matches should be checked for desyncs like any other timing-affecting mod.
  • Latency: a job that completes just after a sleep begins waits for that sleep to end, at most max_ns plus the kernel's wake latency. With the defaults that is under 0.1 ms of a 16.7 ms frame.
  • Not covered by the measurements: loading screens and results, boot-time waits (no eligibility gate; see Install), other 13.0.x images, worker-thread dispatchers (off by default).

Build

Requires the skyline-v3 Rust toolchain and cargo-skyline; rust-toolchain.toml pins the host toolchain.

cargo fmt --check
cargo test
cargo clippy --all-targets -- -D warnings
cargo skyline clippy -- -- -D warnings
cargo skyline build --release
# target/aarch64-skyline-switch/release/libssbu_dispatcher_backoff.nro

src/policy.rs is the host-tested decision logic (7 tests); its decision part is the same code as Deluxe's src/dispatcher_backoff.rs, while the config-file parser exists only here. src/runtime.rs is the Switch-only hook. The repository contains no game assets.

License

MIT. See LICENSE.