Libretro Core Options v2: Negotiate the API Without Losing Settings
Implement libretro core options v2 with correct feature negotiation, stable values, category fallbacks, translations, update handling, and focused tests.
A core option is more than a label in RetroArch’s menu. Its key and machine-readable values form a persistent interface between an emulation core, a frontend, and saved configuration. A careless change can make a familiar setting disappear, silently restore a default, or expose a choice that the core never applies. Version 2 of the libretro core-options API improves presentation through categories and translations, but correct negotiation remains essential.
This article follows the maintained libretro.h contract reviewed on October 11, 2026. Compile against a pinned header in your project and record that revision in test results. The examples illustrate a small core integration, not a claim that every installed frontend supports every environment command.
Negotiate a version before registering definitions
Ask RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION to fill an unsigned. If that environment call returns false, assume version 0. A frontend reporting version 1 receives the older structured definitions through RETRO_ENVIRONMENT_SET_CORE_OPTIONS; a frontend reporting version 2 or later can receive RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2. Version 0 uses the legacy RETRO_ENVIRONMENT_SET_VARIABLES representation.
Keep those branches explicit. An unavailable query does not mean that options themselves are unavailable. Conversely, compiling a recent header does not upgrade the frontend receiving its commands. This distinction is useful when the same core must run on a current desktop frontend and an older appliance image.
The v2 registration return value has a particularly important meaning: it reports whether categories are supported. For a frontend negotiated as v2, false is not a generic registration failure; the contract guarantees registration of the supplied options. Do not respond by registering another representation and accidentally replacing the definitions you just supplied.
Separate persistent identity from presentation
The option key must be unique within the core, and the header recommends a core-name prefix to avoid broader collisions. Valid key characters are letters, digits, underscore, and hyphen. A display label is not the key. Likewise, a translated label for a value is not the machine value that the core reads.
This definition uses C99 designated initializers and the v2 structures:
#include "libretro.h"
static struct retro_core_option_v2_category categories[] = {
{ "video", "Video", "Image presentation settings." },
{ 0 }
};
static struct retro_core_option_v2_definition definitions[] = {
{
.key = "demo_palette",
.desc = "Video Palette",
.desc_categorized = "Palette",
.info = "Choose the palette applied by this example renderer.",
.category_key = "video",
.values = {
{ "default", "Default" },
{ "alternate", "Alternate" },
{ NULL, NULL }
},
.default_value = "default"
},
{ 0 }
};
static struct retro_core_options_v2 options = {
.categories = categories,
.definitions = definitions
};
The category and definition arrays end with zeroed structures. Values normally end with { NULL, NULL }; stay within the header’s maximum value count. The default must equal a machine value in the value array, not its friendly label. An invalid default causes the option to be ignored under the documented contract.
desc must remain meaningful without categories. A short categorized label such as “Palette” works inside a Video group, while “Video Palette” is clearer in a flat list. An absent or unmatched category key makes an option uncategorized. A frontend is not required to implement categories as a particular submenu layout.
Keep registration and capability reporting distinct
The following helper deliberately handles only the negotiated v2 branch. A complete core must implement the v1 and v0 branches separately; returning false here means the caller still needs that older representation, not that options should be omitted.
static bool register_v2(retro_environment_t env,
bool *supports_categories)
{
unsigned version = 0;
*supports_categories = false;
if (!env(RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION, &version))
version = 0;
if (version < 2)
return false;
*supports_categories =
env(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2, &options);
return true;
}
Register options early, ideally in retro_set_environment. The frontend must maintain its own copies of the definitions, strings, and subobjects. That ownership rule permits dynamically constructed definitions, but it does not justify passing malformed pointers or changing an array while it is being consumed.
Registration with a null v2 payload removes existing options. Treat that as an intentional operation rather than a harmless feature probe. The version query is the appropriate mechanism for selecting the representation.
Read values and define when changes take effect
Use RETRO_ENVIRONMENT_GET_VARIABLE with the stable key to obtain the selected machine value. Check both the call result and the returned value before interpreting it. A resilient core has a documented internal default when a value is unavailable, and it rejects values it does not understand instead of making an unchecked array index from user configuration.
RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE lets the core detect changed options and reread the relevant values. Separate parsing from application: a palette may be replaceable immediately, while another option might require rebuilding a subsystem or restarting content. State the actual reset or restart requirement in the option help text and implement the same requirement in the core. A menu change alone is not evidence that the emulated hardware changed.
Visibility is also distinct from availability. SET_CORE_OPTIONS_DISPLAY is a presentation hint; hidden options remain queryable. The update-display callback can let a frontend refresh visibility while the core is not running, and its registration belongs in retro_set_environment. Do not place correctness-critical validation only in the menu visibility path.
Translate labels, not configuration values
For translated v2 definitions, use SET_CORE_OPTIONS_V2_INTL. Its us definitions are required; the local definitions provide translated text. Keys and option values retain their original machine identities. Missing translated text falls back to the corresponding US text, and default values come from the US definitions.
This arrangement allows a user to change menu language without converting a saved value such as alternate into a different token. Translation work should therefore review labels and explanations independently of the compatibility of persistent keys. Changing a key to improve English wording is an API migration, not ordinary copy editing.
Test the protocol before testing the menu
Build a mock environment callback with three version-query behaviors: unavailable, version 1, and version 2. For version 2, return false from v2 registration while recording the supplied definitions. Assert that the helper reports v2 handling complete and categories unsupported, and that it does not issue a second registration command.
Add structural checks for unique keys, valid defaults, matching category identifiers, and properly terminated arrays. Exercise untranslated fallback text and confirm that changing locale does not change machine values. Then run the actual frontend with an isolated configuration, checking both a fresh profile and a profile containing an obsolete saved value. Keep that compatibility experiment separate from your ordinary save directory.
If an option disappears, inspect the definition and negotiated version before deleting configuration. If the menu changes but gameplay does not, trace value retrieval and application timing. If only grouping disappears, inspect the category-support result rather than calling registration again. Preserve the old profile during migration and compare the effective values after a clean restart.
Correct core-options integration is demonstrated by stable settings across API versions, menu layouts, language changes, and reloads. A visually attractive menu is useful, but the underlying configuration contract is what keeps an emulator reproducible.
Related:
- Libretro’s Environment Callback: How Cores Negotiate Features with Frontends
- The Libretro Content-Loading Contract: Paths, Memory, Patches, and Teardown
Sources: