Skip to content

Profile System

The profile system provides button remapping, analog tuning, and trigger configuration. Apps define profiles as static arrays; users cycle between them at runtime. Each player can have an independent active profile.

Source: src/core/services/profiles/profile.h and profile.c

How Profiles Work

A profile is a profile_t struct containing:

  • Button map -- Sparse array of button_map_entry_t entries. Only buttons that differ from the default 1:1 passthrough need entries. If a button is not in the map, it passes through unchanged.
  • Combo map -- button_combo_entry_t entries for multi-button combinations. Combos are checked before individual mappings.
  • Trigger configuration -- Per-trigger behavior (passthrough, digital only, full press, light press, instant, disabled).
  • Analog settings -- Stick sensitivity scaling and button-triggered sensitivity modifiers.
  • SOCD handling -- Simultaneous Opposite Cardinal Direction cleaning (passthrough, neutral, up-priority, last-win).

Button Mapping Types

Simple Remap

Map one input button to a different output button:

MAP_BUTTON(JP_BUTTON_B1, JP_BUTTON_B2)  // A outputs as B

Multi-Button Output

Map one input to multiple output buttons:

MAP_BUTTON_MULTI(JP_BUTTON_R2, JP_BUTTON_L2, JP_BUTTON_R2)  // RT outputs as LT+RT

Button to Analog

Map a button press to an analog axis value:

MAP_BUTTON_ANALOG(JP_BUTTON_L1, JP_BUTTON_L2, ANALOG_TARGET_L2_FULL, 0)  // LB outputs L2 digital + L2 analog at 255
MAP_ANALOG_ONLY(JP_BUTTON_DU, ANALOG_TARGET_RY_MIN)  // D-Up outputs right stick up

Button Disabled

Suppress a button entirely:

MAP_DISABLED(JP_BUTTON_A1)  // Guide button does nothing

Button Combos

Map multiple simultaneous buttons to an output:

MAP_COMBO(JP_BUTTON_L1 | JP_BUTTON_R1, JP_BUTTON_A1)  // L1+R1 = Guide
MAP_COMBO_EXCLUSIVE(JP_BUTTON_B1 | JP_BUTTON_B2, JP_BUTTON_B3)  // A+B = X, but only if EXACTLY A+B pressed

Stick Modifiers

Reduce stick sensitivity when a button is held:

STICK_MODIFIER(JP_BUTTON_L3, 0.5f)  // Hold L3 = 50% stick sensitivity

Trigger Behavior

Each trigger (L2/R2) can be configured independently:

Mode Description
TRIGGER_PASSTHROUGH Analog value passed through, digital activates at threshold
TRIGGER_DIGITAL_ONLY Digital button only, no analog output
TRIGGER_FULL_PRESS Digital + analog forced to 255
TRIGGER_LIGHT_PRESS Analog capped at custom value, no digital
TRIGGER_INSTANT Digital triggers at threshold = 1 (hair trigger)
TRIGGER_DISABLED No output

SOCD Cleaning

For fighting game controllers (Hitbox, arcade sticks) where opposite directions can be pressed simultaneously:

Mode Up+Down Left+Right
SOCD_PASSTHROUGH Both output Both output
SOCD_NEUTRAL Cancel (neutral) Cancel (neutral)
SOCD_UP_PRIORITY Up wins Cancel (neutral)
SOCD_LAST_WIN Last input wins Last input wins

Profile Cycling UX

router_init() installs a set of built-in hotkeys (as default router combos) so profile switching and related tweaks work on every app. Apps that register their own combos (router_set_combo()) take over and replace these defaults.

Hotkey (on the source controller) Action
SELECT + D-pad Up (hold ~0.7 s) Previous profile
SELECT + D-pad Down (hold ~0.7 s) Next profile
SELECT + D-pad Left / Right (hold ~0.7 s) D-pad ↔ stick swap: a 4-position slider that clamps at the ends, [D-pad↔L-stick] [normal] [D-pad↔R-stick] [L-stick↔R-stick] (Left steps toward the first, Right toward the last)
START + D-pad Up (hold ~0.7 s) Shoulder swap toggle (L1↔L2, R1↔R2)

These default hotkeys require a brief ~0.7 s hold (ROUTER_DEFAULT_COMBO_HOLD_MS) before they fire — a quick in-game SELECT/START + D-pad press passes straight through to the console instead of triggering the hotkey (and is not consumed). Once the hold elapses the switch is immediate and clamps at the ends (no wrap). The NeoPixel LED flashes to confirm (number of OFF blinks = profile index + 1) and the controller rumbles if capable. Apps that register their own combo table keep instant, un-held combos.

Per-player profile switching is supported: each player can independently cycle profiles using the same combo on their own controller.

The choices persist to flash (profile index, d-pad mode, shoulder swap) and restore on boot. Profiles can also be selected from the web config. On-the-fly button remapping and rapid-fire have their own hotkeys -- see Runtime Profile.

Profile Persistence

The active profile index is saved to flash and survives power cycles:

  • profile_save_to_flash() writes the index via the storage system
  • profile_load_from_flash() restores it at boot
  • Write debouncing (5-second delay) prevents excessive flash wear from rapid cycling

Applying Profiles

Output drivers call profile_apply() to transform input state through the active profile:

profile_output_t output;
const profile_t* profile = profile_get_active(OUTPUT_TARGET_GAMECUBE);
profile_apply(profile, event->buttons,
              event->analog[ANALOG_LX], event->analog[ANALOG_LY],
              event->analog[ANALOG_RX], event->analog[ANALOG_RY],
              event->analog[ANALOG_L2], event->analog[ANALOG_R2],
              event->analog[ANALOG_RZ], &output);

The profile_output_t contains remapped buttons, analog values with override flags, and passthrough motion/pressure data.

For simple button-only mapping, profile_apply_button_map() returns remapped buttons without analog processing.

Defining Profiles in Apps

Apps define profiles in a profiles.h file:

static const button_map_entry_t my_button_map[] = {
    MAP_BUTTON(JP_BUTTON_B1, JP_BUTTON_B2),
    MAP_BUTTON(JP_BUTTON_B2, JP_BUTTON_B1),
};

static const profile_t my_profiles[] = {
    PROFILE_DEFAULT,  // Index 0: passthrough
    {
        .name = "swapped",
        .description = "A/B buttons swapped",
        .button_map = my_button_map,
        .button_map_count = 2,
        PROFILE_TRIGGERS_DEFAULT,
        PROFILE_ANALOG_DEFAULT,
    },
};

static const profile_set_t my_profile_set = {
    .profiles = my_profiles,
    .profile_count = 2,
    .default_index = 0,
};

The profile set is registered at init via profile_init().

Custom Profiles (Web Config)

In addition to compiled profiles, users can create up to 4 custom profiles via the web configuration tool. Custom profiles are stored in flash as custom_profile_t structs with per-button remap values, stick sensitivity, and SOCD mode. See Storage for the flash format.

See Also

  • Buttons -- JP_BUTTON_* constants used in mappings
  • Router -- How input flows to profiles
  • Storage -- How profile selections persist
  • LEDs -- Visual feedback on profile changes