Smash Ultelier is primarily an interface for smash plugins to the game's render loop but will hopefully grow into a tool for quicker reverse engineering of smash ultimate data structs.
Ideally multiple plugins should be able to talk to the same ssbusync without crashing but its
recommended to only have one plugin interface with the buffer settings.
Most of the time, you should treat SmashUltelier as a code library, not as a standalone plugin.
The default feature set is library-only; the ImGui console is opt-in with the plugin feature.
You only need the shared ssbusync control API.
Import ultelier as a library:
[dependencies]
ultelier = { git = "https://github.com/project-ultelier/smash-ultelier.git", default-features = false, features = ["sync-guest"] }Then make sure to install it in your main function.
#[skyline::main(name = "my-plugin")]
pub fn main() {
let mut config = SsbuSyncConfig::vanilla();
config.overclocker = true;
ultelier::sync_guest::install(config);
}Then use the re-exported guest API:
use ultelier::sync_guest::{self as sync, BufferMode, IndexMode};
pub fn enable_less_lag() {
let _ = sync::set_buffer_mode(BufferMode::Double);
let _ = sync::set_index_mode(IndexMode::OneBehind);
}This is the main reason to use the library.
Use set_buffer_mode(...) to switch between double buffer (less delay) and triple buffer (better performance). Vanilla is triple buffered.
Use set_index_mode(...) to switch between frame index modes. Vanilla is 2 frames behind. Set to 1 frame behind for less delay. Immediate mode (0 frames behind) is performance intensive and only emulators can run it, but you can shave off another frame of delay.
Use set_vsync_enabled(...) to toggle vsync. (disabled = less delay)
Use set_render_opts_enabled(...) to optimize smashed render and input polling loop (enabled = less delay). It is recommended to always enable this when changing buffer/index mode to something other than vanilla.
Basic toggle example:
use ultelier::sync_guest::{self as sync, BufferMode, IndexMode};
pub fn set_low_latency_mode(enabled: bool) {
let buffer_target = match enabled {
true => BufferMode::Double,
false => BufferMode::Triple,
};
let index_target = match enabled {
true => IndexMode::OneBehind, // use IndexMode::Immediate on emulators
false => BufferMode::TwoBehind,
};
let _ = sync::set_vsync_enabled(!enabled);
let _ = sync::set_render_opts_enabled(enabled);
let _ = sync::set_buffer_mode(buffer_target);
let _ = sync::set_index_mode(index_target);
}You can also use the convenience helper:
let _ = ultelier::sync_guest::set_triple_buffer_enabled(true);
let _ = ultelier::sync_guest::set_triple_buffer_enabled(false);That keeps the runtime in the correct mode for later triple/double transitions.
sync_guest::events is the subscription API for runtime change notifications.
set_*/set_typed_*returnsbool:trueif the callback was registered with the remote runtime,falsemeans registration failed.- The callback itself does not return a success value. It is just invoked when the state changes.
Event subscription example:
use ultelier::sync_guest::{self as sync, events, BufferMode, IndexMode};
extern "C" fn on_buffer_mode_changed(mode: BufferMode) {
skyline::println!("buffer mode changed to {:?}", mode);
}
extern "C" fn on_index_mode_changed(mode: IndexMode) {
skyline::println!("index mode changed to {:?}", mode);
}
pub fn subscribe_to_sync_callbacks() -> bool {
events::set_typed_buffer_mode_changed(on_buffer_mode_changed)
&& events::set_typed_index_mode_changed(on_index_mode_changed)
}To unsubscribe:
let _ = ultelier::sync_guest::events::clear_typed_buffer_mode_changed();
let _ = ultelier::sync_guest::events::clear_typed_index_mode_changed();
let _ = ultelier::sync_guest::events::clear_typed_vsync_changed();
let _ = ultelier::sync_guest::events::clear_typed_render_opts_changed();sync_guest also exposes helpers for default and runtime resolution control.
Default game resolution configuration:
set_default_resolution_level(level)sets the baseline internal render level.default_resolution_level()reads the configured baseline asOption<ResolutionLevel>.default_game_resolution()reads the concreteResolution { width, height }for the baseline.
Dynamic resolution control:
set_dynamic_resolution_level(Some(level))enables dynamic resolution and sets the level that is applied while at least one report is outstanding.set_dynamic_resolution_level(None)disables dynamic resolution and drops all outstanding reports.dynamic_resolution_level()reads the configured dynamic level asOption<ResolutionLevel>(Nonewhen disabled).dynamic_resolution_enabled()reads whether dynamic resolution is currently enabled.push_dynamic_res_report()registers a report; the dynamic level is applied while the report count is above zero. ReturnsSome(false)if dynamic resolution is disabled.pop_dynamic_res_report()releases one report. Every push must be balanced by a pop. ReturnsSome(false)if dynamic resolution is disabled or there was nothing to release.clear_all_dynamic_res_report()drops all outstanding reports without disabling dynamic resolution.current_game_resolution()reads the currently applied resolution: the dynamic level when enabled and at least one report is outstanding, otherwise the default level.apparent_game_resolution()reads the actual/effective resolution of the last presented frame.
Example:
use ultelier::sync_guest::{self as sync, ResolutionLevel};
pub fn on_game_start() {
let _ = sync::set_default_resolution_level(ResolutionLevel::Res1920x1080);
let _ = sync::set_dynamic_resolution_level(Some(ResolutionLevel::Res1280x720));
}
pub fn on_game_frame() {
if intensive_effect_started() {
let _ = sync::push_dynamic_res_report();
} else if intensive_effect_ended() {
let _ = sync::pop_dynamic_res_report();
}
}
pub fn on_match_end() {
// Drop any reports that were never popped, but keep dynamic resolution configured.
let _ = sync::clear_all_dynamic_res_report();
}
pub fn on_game_end() {
// Disabling also drops any outstanding reports.
let _ = sync::set_dynamic_resolution_level(None);
}