From 631c4438923cebfb22ee8c8056edb35a72b2b859 Mon Sep 17 00:00:00 2001 From: LibretroAdmin Date: Thu, 15 Oct 2026 22:40:17 +0200 Subject: [PATCH] core options, runahead: one allocation for value hashes, flat input list Option value hashes live in one pool per manager, indexed by value, and every value and label append is checked, so a failed append ends the parse without writing outside a list. The legacy parser reads the description and values in place from the variable string. Runahead's input logging keeps the tuples the dense cache has no slot for in one flat array; each tuple's ids are sized on first write. samples/core_options drives the manager with each append failing in turn; samples/runahead covers the input list, including each realloc failing in turn. --- core_option_manager.c | 187 ++++++----- core_option_manager.h | 5 + input/input_driver.h | 9 - runahead.c | 409 +++++++---------------- runahead.h | 12 - runloop.h | 1 - samples/core_options/Makefile | 67 ++++ samples/core_options/core_options_test.c | 322 ++++++++++++++++++ samples/runahead/Makefile | 7 +- samples/runahead/runahead_copy_test.c | 95 ++++++ 10 files changed, 720 insertions(+), 394 deletions(-) create mode 100644 samples/core_options/Makefile create mode 100644 samples/core_options/core_options_test.c diff --git a/core_option_manager.c b/core_option_manager.c index ea35cbcc56..af9a670011 100644 --- a/core_option_manager.c +++ b/core_option_manager.c @@ -644,19 +644,51 @@ static const char *core_option_manager_parse_value_label( return label; } +/* Returns the start of the value list of a legacy + * core options variable ("; ||..."), + * or NULL if it has none */ +static const char *core_option_manager_var_values( + const char *value, size_t *desc_len) +{ + const char *p; + if (!value) + return NULL; + for (p = value; *p; p++) + { + if (p[0] == ';' && p[1] == ' ') + { + *desc_len = (size_t)(p - value); + return p + 2; + } + } + return NULL; +} + +/* Upper bound on the number of values string_split() + * extracts from a legacy value list */ +static size_t core_option_manager_var_num_values(const char *val_start) +{ + size_t n = 1; + for (; *val_start; val_start++) + if (*val_start == '|') + n++; + return n; +} + /* Parses a single legacy core options interface * variable, extracting all present core_option - * information */ + * information. @val_hashes has room for every value + * the variable defines. */ static bool core_option_manager_parse_variable( core_option_manager_t *opt, size_t idx, const struct retro_variable *var, - config_file_t *config_src) + config_file_t *config_src, + uint32_t *val_hashes) { size_t i; union string_list_elem_attr attr; + size_t desc_len = 0; const char *val_start = NULL; - char *value = NULL; - char *desc_end = NULL; struct core_option *option = (struct core_option*)&opt->opts[idx]; struct config_entry_list *entry = NULL; @@ -674,45 +706,31 @@ static bool core_option_manager_parse_variable( option->key_hash = core_option_manager_hash_string(var->key); } - if (var->value && *var->value) - value = strdup(var->value); + if (!(val_start = core_option_manager_var_values(var->value, &desc_len))) + return false; - if (value && *value) + if (desc_len > 0) { - char *p = value; - while (*p) - { - if (p[0] == ';' && p[1] == ' ') - { - desc_end = p; - break; - } - p++; - } + if (!(option->desc = (char*)malloc(desc_len + 1))) + return false; + memcpy(option->desc, var->value, desc_len); + option->desc[desc_len] = '\0'; } - if (!desc_end) - goto error; - - *desc_end = '\0'; - - if (value && *value) - option->desc = strdup(value); - - val_start = desc_end + 2; option->vals = string_split(val_start, "|"); if (!option->vals) - goto error; + return false; /* Legacy core option interface has no concept * of value labels * > Use actual values for display purposes */ attr.i = 0; option->val_labels = string_list_new(); + option->val_hashes = val_hashes; if (!option->val_labels) - goto error; + return false; /* > Loop over values and: * - Set value hashes @@ -720,23 +738,18 @@ static bool core_option_manager_parse_variable( for (i = 0; i < option->vals->size; i++) { const char *value = option->vals->elems[i].data; - uint32_t *value_hash = (uint32_t *)malloc(sizeof(uint32_t)); const char *value_label = core_option_manager_parse_value_label( value, NULL); - if (!value_hash) - goto error; - - /* Set value hash */ - *value_hash = core_option_manager_hash_string(value); - option->vals->elems[i].userdata = (void*)value_hash; + val_hashes[i] = core_option_manager_hash_string(value); /* Redundant safely check... */ if (!value_label || !*value_label) value_label = value; /* Append value label string */ - string_list_append(option->val_labels, value_label, attr); + if (!string_list_append(option->val_labels, value_label, attr)) + return false; } /* Legacy core option interface always uses first @@ -756,11 +769,8 @@ static bool core_option_manager_parse_variable( for (i = 0; i < option->vals->size; i++) { - const char *value = option->vals->elems[i].data; - uint32_t value_hash = *((uint32_t*)option->vals->elems[i].userdata); - - if ( value_hash == entry_value_hash - && string_is_equal(value, entry->value)) + if ( val_hashes[i] == entry_value_hash + && string_is_equal(option->vals->elems[i].data, entry->value)) { option->index = i; break; @@ -774,12 +784,7 @@ static bool core_option_manager_parse_variable( option->info_categorized = NULL; option->category_key = NULL; - free(value); return true; - -error: - free(value); - return false; } /** @@ -804,6 +809,7 @@ core_option_manager_t *core_option_manager_new_vars( { const struct retro_variable *var = NULL; size_t _len = 0; + size_t num_vals = 0; config_file_t *config_src = NULL; core_option_manager_t *opt = NULL; @@ -822,6 +828,7 @@ core_option_manager_t *core_option_manager_new_vars( opt->cats_size = 0; opt->opts = NULL; opt->size = 0; + opt->val_hashes = NULL; opt->option_map = nested_list_init(); opt->updated = false; opt->log = true; @@ -841,24 +848,35 @@ core_option_manager_t *core_option_manager_new_vars( if (src_conf_path && *src_conf_path) config_src = config_file_new_from_path_to_string(src_conf_path); - /* Get number of variables */ + /* Get number of variables and values */ for (var = vars; var->key && var->value; var++) + { + size_t desc_len = 0; + const char *val_start = core_option_manager_var_values( + var->value, &desc_len); + if (val_start) + num_vals += core_option_manager_var_num_values(val_start); _len++; + } - if (_len == 0) + if (_len == 0 || num_vals == 0) goto error; - /* Create options array */ + /* Create options array and value hash pool */ if (!(opt->opts = (struct core_option*)calloc(_len, sizeof(*opt->opts)))) goto error; + if (!(opt->val_hashes = (uint32_t*)malloc(num_vals * sizeof(uint32_t)))) + goto error; opt->size = _len; _len = 0; + num_vals = 0; /* Parse each variable */ for (var = vars; var->key && var->value; _len++, var++) { - if (core_option_manager_parse_variable(opt, _len, var, config_src)) + if (core_option_manager_parse_variable(opt, _len, var, config_src, + opt->val_hashes + num_vals)) { size_t __len = 0; /* If variable is read correctly, add it to @@ -880,6 +898,7 @@ core_option_manager_t *core_option_manager_new_vars( if (!nested_list_add_item(opt->option_map, address, NULL, (const void*)&opt->opts[_len])) goto error; + num_vals += opt->opts[_len].vals->size; } else goto error; @@ -897,13 +916,26 @@ error: return NULL; } +/* Number of values a v2 option definition lists */ +static size_t core_option_manager_def_num_values( + const struct retro_core_option_value *values) +{ + size_t n = 0; + while ( n < RETRO_NUM_CORE_OPTION_VALUES_MAX + && values[n].value && *values[n].value) + n++; + return n; +} + /* Parses a single v2 core options interface * option, extracting all present core_option - * information */ + * information. @val_hashes has room for every value + * the option defines. */ static bool core_option_manager_parse_option( core_option_manager_t *opt, size_t idx, const struct retro_core_option_v2_definition *option_def, - config_file_t *config_src) + config_file_t *config_src, + uint32_t *val_hashes) { size_t i; union string_list_elem_attr attr; @@ -974,21 +1006,14 @@ static bool core_option_manager_parse_option( option->key_hash = core_option_manager_hash_string(key); } - /* Get number of values */ - for (;;) - { - if (!values[num_vals].value || !*values[num_vals].value) - break; - num_vals++; - } - - if (num_vals < 1) + if ((num_vals = core_option_manager_def_num_values(values)) < 1) return false; /* Initialise string lists */ attr.i = 0; option->vals = string_list_new(); option->val_labels = string_list_new(); + option->val_hashes = val_hashes; if (!option->vals || !option->val_labels) return false; @@ -1001,19 +1026,15 @@ static bool core_option_manager_parse_option( for (i = 0; i < num_vals; i++) { const char *value = values[i].value; - uint32_t *value_hash = (uint32_t *)malloc(sizeof(uint32_t)); const char *value_label = values[i].label; - if (!value_hash) - return false; - /* Append value string * > We know that 'value' is always valid */ - string_list_append(option->vals, value, attr); + if (!string_list_append(option->vals, value, attr)) + return false; /* > Set value hash */ - *value_hash = core_option_manager_hash_string(value); - option->vals->elems[option->vals->size - 1].userdata = (void*)value_hash; + val_hashes[i] = core_option_manager_hash_string(value); /* Value label requires additional processing */ value_label = core_option_manager_parse_value_label( @@ -1024,7 +1045,8 @@ static bool core_option_manager_parse_option( value_label = value; /* Append value label string */ - string_list_append(option->val_labels, value_label, attr); + if (!string_list_append(option->val_labels, value_label, attr)) + return false; /* Check whether this value is the default setting */ if (option_def->default_value && *option_def->default_value) @@ -1049,11 +1071,8 @@ static bool core_option_manager_parse_option( for (i = 0; i < option->vals->size; i++) { - const char *value = option->vals->elems[i].data; - uint32_t value_hash = *((uint32_t*)option->vals->elems[i].userdata); - - if ( value_hash == entry_value_hash - && string_is_equal(value, entry->value)) + if ( val_hashes[i] == entry_value_hash + && string_is_equal(option->vals->elems[i].data, entry->value)) { option->index = i; break; @@ -1095,6 +1114,7 @@ core_option_manager_t *core_option_manager_new( struct retro_core_option_v2_definition *option_defs = NULL; size_t cats_size = 0; size_t _len = 0; + size_t num_vals = 0; config_file_t *config_src = NULL; core_option_manager_t *opt = NULL; @@ -1114,6 +1134,7 @@ core_option_manager_t *core_option_manager_new( opt->cats_size = 0; opt->opts = NULL; opt->size = 0; + opt->val_hashes = NULL; opt->option_map = nested_list_init(); opt->updated = false; opt->log = true; @@ -1149,9 +1170,12 @@ core_option_manager_t *core_option_manager_new( for ( option_def = option_defs; option_def->key && option_def->desc && option_def->values[0].value; option_def++) + { + num_vals += core_option_manager_def_num_values(option_def->values); _len++; + } - if (_len == 0) + if (_len == 0 || num_vals == 0) goto error; /* Create categories array */ @@ -1180,12 +1204,15 @@ core_option_manager_t *core_option_manager_new( } } - /* Create options array */ + /* Create options array and value hash pool */ if (!(opt->opts = (struct core_option*)calloc(_len, sizeof(*opt->opts)))) goto error; + if (!(opt->val_hashes = (uint32_t*)malloc(num_vals * sizeof(uint32_t)))) + goto error; opt->size = _len; _len = 0; + num_vals = 0; /* Parse each option * > Note: 'option_def->info == NULL' is valid */ @@ -1193,7 +1220,8 @@ core_option_manager_t *core_option_manager_new( option_def->key && option_def->desc && option_def->values[0].value; _len++, option_def++) { - if (core_option_manager_parse_option(opt, _len, option_def, config_src)) + if (core_option_manager_parse_option(opt, _len, option_def, config_src, + opt->val_hashes + num_vals)) { /* If option is read correctly, add it to * the map */ @@ -1221,6 +1249,7 @@ core_option_manager_t *core_option_manager_new( address, ":", (const void*)&opt->opts[_len])) goto error; + num_vals += opt->opts[_len].vals->size; } else goto error; @@ -1307,6 +1336,7 @@ void core_option_manager_free(core_option_manager_t *opt) free(opt->cats); free(opt->opts); + free(opt->val_hashes); free(opt); } @@ -1519,9 +1549,8 @@ bool core_option_manager_get_val_idx(core_option_manager_t *opt, for (i = 0; i < option->vals->size; i++) { const char *option_val = option->vals->elems[i].data; - uint32_t option_val_hash = *((uint32_t*)option->vals->elems[i].userdata); - if ( (val_hash == option_val_hash) + if ( (val_hash == option->val_hashes[i]) && (option_val && *option_val) && string_is_equal(val, option_val)) { diff --git a/core_option_manager.h b/core_option_manager.h index c306016e99..7f16dcac6e 100644 --- a/core_option_manager.h +++ b/core_option_manager.h @@ -40,6 +40,9 @@ struct core_option char *category_key; struct string_list *vals; struct string_list *val_labels; + /* val_hashes[i]: hash of vals->elems[i].data; points + * into the manager's val_hashes pool */ + uint32_t *val_hashes; /* opt_idx: option index, used for internal * bookkeeping */ size_t opt_idx; @@ -71,6 +74,8 @@ struct core_option_manager struct core_category *cats; struct core_option *opts; nested_list_t *option_map; + /* Value hashes of every option, one allocation */ + uint32_t *val_hashes; size_t cats_size; size_t size; diff --git a/input/input_driver.h b/input/input_driver.h index 018e14fa92..6dc29dac41 100644 --- a/input/input_driver.h +++ b/input/input_driver.h @@ -394,15 +394,6 @@ typedef struct input_remote_state int16_t analog[4][MAX_USERS]; } input_remote_state_t; -typedef struct input_list_element_t -{ - int16_t *state; - unsigned port; - unsigned device; - unsigned index; - unsigned int state_size; -} input_list_element; - /** * Organizes the functions and data structures of each driver that are accessed * by other parts of the input code. The input_driver structs are the "interface" diff --git a/runahead.c b/runahead.c index a274878765..2584de4007 100644 --- a/runahead.c +++ b/runahead.c @@ -88,39 +88,134 @@ static int16_t *runahead_dense_slot(runahead_dense_cache_t *c, } /* ===== END runahead dense input cache ===== */ +/* ===== BEGIN runahead input list ===== + * Tuples the dense cache has no slot for, one flat array; each + * tuple's ids are sized by the highest id written to it. */ static runahead_dense_cache_t *runahead_dense; +/* Last value of every id of one (port, device, index) + * tuple; ids past state_size read 0 */ +typedef struct input_list_element +{ + int16_t *state; + unsigned port; + unsigned device; + unsigned index; + unsigned state_size; +} input_list_element; + +typedef struct runahead_input_list +{ + input_list_element *elems; + unsigned size; + unsigned cap; +} runahead_input_list_t; + +static runahead_input_list_t runahead_input_list; + +/* The list entry of a (port, device, index) tuple, or NULL */ +static input_list_element *runahead_input_list_find( + const runahead_input_list_t *list, + unsigned port, unsigned device, unsigned index) +{ + unsigned i; + for (i = 0; i < list->size; i++) + { + input_list_element *element = &list->elems[i]; + if ( (element->port == port) + && (element->device == device) + && (element->index == index)) + return element; + } + return NULL; +} + +static void runahead_input_list_free(runahead_input_list_t *list) +{ + unsigned i; + for (i = 0; i < list->size; i++) + free(list->elems[i].state); + free(list->elems); + list->elems = NULL; + list->size = 0; + list->cap = 0; +} + +/* Grows the state of @element so that @id indexes it; + * the new ids read 0 */ +static bool runahead_input_element_expand( + input_list_element *element, unsigned id) +{ + int16_t *state; + unsigned new_size = element->state_size ? element->state_size : 32; + while (id >= new_size) + new_size *= 2; + if (!(state = (int16_t*)realloc(element->state, + new_size * sizeof(int16_t)))) + return false; + memset(&state[element->state_size], 0, + (new_size - element->state_size) * sizeof(int16_t)); + element->state = state; + element->state_size = new_size; + return true; +} + +static void runahead_input_state_set_last( + unsigned port, unsigned device, + unsigned index, unsigned id, int16_t value) +{ + runahead_input_list_t *list = &runahead_input_list; + input_list_element *element; + int16_t *slot = runahead_dense_slot(runahead_dense, port, device, index, id); + + if (slot) + { + *slot = value; + return; + } + + if (!(element = runahead_input_list_find(list, port, device, index))) + { + if (list->size == list->cap) + { + unsigned new_cap = list->cap ? list->cap * 2 : 8; + input_list_element *elems = (input_list_element*)realloc( + list->elems, new_cap * sizeof(*elems)); + if (!elems) + return; + list->elems = elems; + list->cap = new_cap; + } + element = &list->elems[list->size++]; + element->state = NULL; + element->port = port; + element->device = device; + element->index = index; + element->state_size = 0; + } + + if ( id >= element->state_size + && !runahead_input_element_expand(element, id)) + return; + element->state[id] = value; +} + static int16_t input_state_get_last(unsigned port, unsigned device, unsigned index, unsigned id) { - runloop_state_t *runloop_st = runloop_state_get_ptr(); + const input_list_element *element; const int16_t *slot = runahead_dense_slot(runahead_dense, port, device, index, id); if (slot) return *slot; - if (runloop_st->input_state_list) - { - int i; - /* find list item */ - for (i = 0; i < runloop_st->input_state_list->size; i++) - { - input_list_element *element = - (input_list_element*)runloop_st->input_state_list->data[i]; - - if ( (element->port == port) - && (element->device == device) - && (element->index == index)) - { - if (id < element->state_size) - return element->state[id]; - break; - } - } - } - + element = runahead_input_list_find( + &runahead_input_list, port, device, index); + if (element && id < element->state_size) + return element->state[id]; return 0; } +/* ===== END runahead input list ===== */ static void free_retro_ctx_load_content_info(struct retro_ctx_load_content_info *dest) @@ -1170,271 +1265,6 @@ void runahead_remember_controller_port_device(void *data, } #endif -static void mylist_resize(my_list *list, - int new_size, bool run_constructor) -{ - int i; - int new_capacity; - int old_size; - void *element = NULL; - if (new_size < 0) - new_size = 0; - new_capacity = new_size; - old_size = list->size; - - if (new_size == old_size) - return; - - if (new_size > list->capacity) - { - void **new_data; - - if (new_capacity < list->capacity * 2) - new_capacity = list->capacity * 2; - - /* Try to realloc. On OOM, leave the list at its current - * capacity and silently no-op the resize - the caller - * (mylist_add_element) tolerates list->data[old_size] - * being NULL: runahead_input_state_set_last guards on - * 'if (element)' before writing to it, and downstream - * code already accepts that runahead state may be - * incomplete. */ - new_data = (void**)realloc( - (void*)list->data, new_capacity * sizeof(void*)); - if (!new_data) - return; - list->data = new_data; - - for (i = list->capacity; i < new_capacity; i++) - list->data[i] = NULL; - - list->capacity = new_capacity; - } - - if (new_size <= list->size) - { - for (i = new_size; i < list->size; i++) - { - element = list->data[i]; - - if (element) - { - list->destructor(element); - list->data[i] = NULL; - } - } - } - else - { - for (i = list->size; i < new_size; i++) - { - list->data[i] = NULL; - if (run_constructor) - list->data[i] = list->constructor(); - } - } - - list->size = new_size; -} - -static void *mylist_add_element(my_list *list) -{ - int old_size; - if (!list) - return NULL; - old_size = list->size; - mylist_resize(list, old_size + 1, true); - /* mylist_resize may have failed to grow on OOM, in which case - * list->size is still old_size and list->data[old_size] is - * out of bounds. Re-check before returning. */ - if (list->size <= old_size) - return NULL; - return list->data[old_size]; -} - -static void mylist_destroy(my_list **list_p) -{ - my_list *list = NULL; - if (!list_p) - return; - - list = *list_p; - - if (list) - { - mylist_resize(list, 0, false); - free(list->data); - free(list); - *list_p = NULL; - } -} - -static void mylist_create(my_list **list_p, int initial_capacity, - constructor_t constructor, destructor_t destructor) -{ - my_list *list = NULL; - - if (!list_p) - return; - - list = *list_p; - if (list) - mylist_destroy(list_p); - - list = (my_list*)malloc(sizeof(my_list)); - if (!list) - { - *list_p = NULL; - return; - } - *list_p = list; - list->size = 0; - list->constructor = constructor; - list->destructor = destructor; - list->data = (void**)calloc(initial_capacity, sizeof(void*)); - /* On calloc OOM, leave list->data NULL and capacity 0; - * mylist_resize's realloc grows from there on first add and - * a subsequent realloc(NULL, n) is well-defined. */ - list->capacity = list->data ? initial_capacity : 0; -} - -static void *input_list_element_constructor(void) -{ - void *ptr = malloc(sizeof(input_list_element)); - input_list_element *element; - - /* NULL-check the outer malloc: the field writes below - * (port/device/index/state_size) would NULL-deref on OOM. - * The caller (runahead_input_state_set_last) already tolerates - * a NULL return: 'if (element) { ... }'. mylist_resize also - * tolerates a NULL constructor result - it just leaves - * list->data[i] = NULL. */ - if (!ptr) - return NULL; - - element = (input_list_element*)ptr; - element->port = 0; - element->device = 0; - element->index = 0; - element->state = (int16_t*)calloc(NAME_MAX_LENGTH, - sizeof(int16_t)); - /* NULL-check the inner calloc too. If it fails the caller's - * 'element->state[id] = value' NULL-derefs. No way to signal - * partial-failure through the constructor callback's return - * type, so free the outer allocation and return NULL to match - * the outer-OOM behaviour. */ - if (!element->state) - { - free(ptr); - return NULL; - } - element->state_size = NAME_MAX_LENGTH; - - return ptr; -} - -static bool input_list_element_realloc(input_list_element *element, - unsigned int new_size) -{ - if (new_size > element->state_size) - { - /* realloc-to-tmp: the pre-patch 'element->state = realloc( - * element->state, ...)' self-assigns NULL on OOM, which - * then made the very next line '&element->state[element-> - * state_size]' perform pointer arithmetic on NULL (UB) and - * the memset trap on a garbage address. */ - int16_t *tmp = (int16_t*)realloc(element->state, - new_size * sizeof(int16_t)); - if (!tmp) - return false; - element->state = tmp; - memset(&element->state[element->state_size], 0, - (new_size - element->state_size) * sizeof(int16_t)); - element->state_size = new_size; - } - return true; -} - -static bool input_list_element_expand(input_list_element *element, - unsigned int new_index) -{ - unsigned int new_size = element->state_size; - if (new_size == 0) - new_size = 32; - while (new_index >= new_size) - new_size *= 2; - return input_list_element_realloc(element, new_size); -} - -static void input_list_element_destructor(void* element_ptr) -{ - input_list_element *element = (input_list_element*)element_ptr; - if (!element) - return; - - free(element->state); - free(element_ptr); -} - -static void runahead_input_state_set_last( - runloop_state_t *runloop_st, - unsigned port, unsigned device, - unsigned index, unsigned id, int16_t value) -{ - size_t i; - input_list_element *element = NULL; - int16_t *slot = runahead_dense_slot(runahead_dense, port, device, index, id); - - if (slot) - { - *slot = value; - return; - } - - if (!runloop_st->input_state_list) - mylist_create(&runloop_st->input_state_list, 16, - input_list_element_constructor, - input_list_element_destructor); - if (!runloop_st->input_state_list) - return; - - /* Find list item */ - for (i = 0; i < (unsigned)runloop_st->input_state_list->size; i++) - { - element = (input_list_element*)runloop_st->input_state_list->data[i]; - if ( (element->port == port) - && (element->device == device) - && (element->index == index) - ) - { - /* Gate the state[id] write on expand success: if expand - * OOM'd, state_size is still too small and writing to - * state[id] would corrupt memory past the buffer. */ - if (id >= element->state_size - && !input_list_element_expand(element, id)) - return; - element->state[id] = value; - return; - } - } - - element = NULL; - if (runloop_st->input_state_list) - element = (input_list_element*) - mylist_add_element(runloop_st->input_state_list); - if (element) - { - element->port = port; - element->device = device; - element->index = index; - /* Same expand-OOM guard as the lookup branch above. */ - if (id >= element->state_size - && !input_list_element_expand(element, id)) - return; - element->state[id] = value; - } -} - static int16_t runahead_input_state_with_logging(unsigned port, unsigned device, unsigned index, unsigned id) { @@ -1451,7 +1281,7 @@ static int16_t runahead_input_state_with_logging(unsigned port, runloop_st->flags |= RUNLOOP_FLAG_INPUT_IS_DIRTY; /*arbitrary limit of up to 65536 elements in state array*/ if (id < 65536) - runahead_input_state_set_last(runloop_st, port, device, index, id, result); + runahead_input_state_set_last(port, device, index, id, result); return result; } return 0; @@ -1513,13 +1343,10 @@ static void runahead_remove_input_state_hook(runloop_state_t *runloop_st) runloop_st->input_state_callback_original; runloop_st->current_core.retro_set_input_state(cbs->state_cb); runloop_st->input_state_callback_original = NULL; - mylist_destroy(&runloop_st->input_state_list); - if (runahead_dense) - { + runahead_input_list_free(&runahead_input_list); free(runahead_dense); runahead_dense = NULL; } - } if (runloop_st->retro_reset_callback_original) { diff --git a/runahead.h b/runahead.h index cdddfad00a..e8c6c6ddbf 100644 --- a/runahead.h +++ b/runahead.h @@ -26,18 +26,6 @@ #define MAX_RUNAHEAD_FRAMES 12 -typedef void *(*constructor_t)(void); -typedef void (*destructor_t )(void*); - -typedef struct my_list_t -{ - void **data; - constructor_t constructor; - destructor_t destructor; - int capacity; - int size; -} my_list; - typedef struct preemptive_frames_data { /* Savestate buffer */ diff --git a/runloop.h b/runloop.h index e13a9c6d64..8009a4e630 100644 --- a/runloop.h +++ b/runloop.h @@ -259,7 +259,6 @@ struct runloop * per access, and a constructor/destructor function-pointer pair * for what amounts to alloc/free of a data buffer. */ retro_ctx_serialize_info_t runahead_savestate_info; - my_list *input_state_list; preempt_t *preempt_data; #endif diff --git a/samples/core_options/Makefile b/samples/core_options/Makefile new file mode 100644 index 0000000000..0024f0e190 --- /dev/null +++ b/samples/core_options/Makefile @@ -0,0 +1,67 @@ +include ../sample_objdir.mk + +TARGET := core_options_test + +CORE_DIR = ../.. +LIBRETRO_COMM_DIR = $(CORE_DIR)/libretro-common +INCDIRS := -I$(LIBRETRO_COMM_DIR)/include -I$(CORE_DIR) -I$(CORE_DIR)/deps + +CC ?= gcc + +# core_option_manager.c as shipped, on the real config_file, +# string_list and nested_list. string_list_append is wrapped so the +# test can fail any one call. +SOURCES_C := \ + core_options_test.c \ + $(CORE_DIR)/core_option_manager.c \ + $(LIBRETRO_COMM_DIR)/lists/string_list.c \ + $(LIBRETRO_COMM_DIR)/lists/nested_list.c \ + $(LIBRETRO_COMM_DIR)/file/config_file.c \ + $(LIBRETRO_COMM_DIR)/file/config_file_io.c \ + $(LIBRETRO_COMM_DIR)/file/file_path.c \ + $(LIBRETRO_COMM_DIR)/file/file_path_io.c \ + $(LIBRETRO_COMM_DIR)/streams/file_stream.c \ + $(LIBRETRO_COMM_DIR)/vfs/vfs_implementation.c \ + $(LIBRETRO_COMM_DIR)/string/stdstring.c \ + $(LIBRETRO_COMM_DIR)/string/rstrtod.c \ + $(LIBRETRO_COMM_DIR)/compat/compat_strl.c \ + $(LIBRETRO_COMM_DIR)/compat/compat_strldup.c \ + $(LIBRETRO_COMM_DIR)/compat/compat_posix_string.c \ + $(LIBRETRO_COMM_DIR)/compat/fopen_utf8.c \ + $(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c \ + $(LIBRETRO_COMM_DIR)/time/rtime.c + +CFLAGS += -std=gnu99 -O1 -g1 -Wall -Werror=implicit-function-declaration \ + $(INCDIRS) -DRARCH_INTERNAL -DHAVE_CONFIGFILE -D_GNU_SOURCE +LDFLAGS += -Wl,--wrap=string_list_append + +ifneq ($(SAN),) + SANITIZER := $(SAN) +endif +ifneq ($(SANITIZER),) + CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS) + LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS) +endif + +OBJS := $(call sample_objs,$(SOURCES_C:.c=.o)) + +all: $(TARGET) + +%.o: %.c + $(CC) $(CFLAGS) -c -o $@ $< + +$(SAMPLE_OBJDIR)/%.o: $$(call sample_src,$$*).c + @mkdir -p $(@D) + $(CC) $(CFLAGS) -c -o $@ $< + +$(TARGET): $(OBJS) + $(CC) -o $@ $(OBJS) $(LDFLAGS) -lm + +check: $(TARGET) + ./$(TARGET) + +clean: + rm -rf $(SAMPLE_OBJDIR) + rm -f $(OBJS) $(TARGET) $(TARGET).exe core_options_test.cfg + +.PHONY: all check clean diff --git a/samples/core_options/core_options_test.c b/samples/core_options/core_options_test.c new file mode 100644 index 0000000000..e648d9bacb --- /dev/null +++ b/samples/core_options/core_options_test.c @@ -0,0 +1,322 @@ +/* The core option manager, driven directly. + * + * core_option_manager.c is linked as shipped against the real + * libretro-common config_file, string_list and nested_list. + * string_list_append is wrapped (--wrap) so a test can make the + * Nth append fail and check that the manager refuses the options + * cleanly - no write outside a list, no leak - under ASan. */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "../../core_option_manager.h" +#include "../../msg_hash.h" +#include "../../verbosity.h" + +static unsigned failures = 0; +#define CHECK(cond, msg) do { if (!(cond)) { printf(" FAIL: %s\n", msg); failures++; } } while (0) + +/* --- what core_option_manager.c needs from the frontend ------------- */ + +const char *msg_hash_to_str(enum msg_hash_enums msg) +{ + switch (msg) + { + case MENU_ENUM_LABEL_VALUE_ON: + return "ON"; + case MENU_ENUM_LABEL_VALUE_OFF: + return "OFF"; + case MENU_ENUM_LABEL_ENABLED: + return "enabled"; + case MENU_ENUM_LABEL_DISABLED: + return "disabled"; + default: + break; + } + return "\x01"; +} + +void RARCH_DBG(const char *fmt, ...) { (void)fmt; } + +/* --- the failing append ------------------------------------------- */ + +bool __real_string_list_append(struct string_list *list, const char *elem, + union string_list_elem_attr attr); + +/* 0: never fail; N: the Nth append from now fails */ +static unsigned append_fail_at; +static unsigned append_calls; + +bool __wrap_string_list_append(struct string_list *list, const char *elem, + union string_list_elem_attr attr) +{ + append_calls++; + if (append_fail_at && append_calls == append_fail_at) + return false; + return __real_string_list_append(list, elem, attr); +} + +static void fail_append(unsigned n) +{ + append_calls = 0; + append_fail_at = n; +} + +/* --- fixtures ------------------------------------------------------ */ + +#define CONF_PATH "core_options_test.cfg" + +static void write_conf(const char *text) +{ + FILE *f = fopen(CONF_PATH, "wb"); + if (f) + { + fputs(text, f); + fclose(f); + } +} + +static size_t find_option(core_option_manager_t *opt, const char *key) +{ + size_t i; + for (i = 0; i < opt->size; i++) + if (opt->opts[i].key && !strcmp(opt->opts[i].key, key)) + return i; + return (size_t)-1; +} + +static struct retro_core_option_v2_definition v2_defs[4]; +static struct retro_core_options_v2 v2_opts; + +static void build_v2(void) +{ + memset(v2_defs, 0, sizeof(v2_defs)); + + v2_defs[0].key = "t_res"; + v2_defs[0].desc = "Resolution"; + v2_defs[0].values[0].value = "320x240"; + v2_defs[0].values[1].value = "640x480"; + v2_defs[0].values[1].label = "Double"; + v2_defs[0].values[2].value = "1280x960"; + v2_defs[0].default_value = "640x480"; + + v2_defs[1].key = "t_flag"; + v2_defs[1].desc = "Flag"; + v2_defs[1].values[0].value = "disabled"; + v2_defs[1].values[1].value = "enabled"; + v2_defs[1].default_value = "disabled"; + + /* Every value slot filled: the count stops at the array's end */ + { + static char vals[RETRO_NUM_CORE_OPTION_VALUES_MAX][12]; + int i; + v2_defs[2].key = "t_full"; + v2_defs[2].desc = "Full"; + for (i = 0; i < RETRO_NUM_CORE_OPTION_VALUES_MAX; i++) + { + snprintf(vals[i], sizeof(vals[i]), "%d", i); + v2_defs[2].values[i].value = vals[i]; + } + v2_defs[2].default_value = "5"; + } + + v2_opts.categories = NULL; + v2_opts.definitions = v2_defs; +} + +/* --- v2 interface -------------------------------------------------- */ + +static void t_v2(void) +{ + core_option_manager_t *opt; + size_t i, idx; + + printf(" v2: values, defaults, config value and lookup\n"); + build_v2(); + write_conf("t_flag = \"enabled\"\n"); + fail_append(0); + + opt = core_option_manager_new(CONF_PATH, NULL, &v2_opts, false); + CHECK(opt != NULL, "manager not created"); + if (!opt) + return; + CHECK(opt->size == 3, "option count"); + + i = find_option(opt, "t_res"); + CHECK(i != (size_t)-1, "t_res missing"); + if (i != (size_t)-1) + { + CHECK(opt->opts[i].vals->size == 3, "t_res value count"); + CHECK(opt->opts[i].val_labels->size == 3, "t_res label count"); + CHECK(opt->opts[i].index == 1, "t_res default not applied"); + CHECK(!strcmp(core_option_manager_get_val(opt, i), "640x480"), "t_res value"); + CHECK(!strcmp(core_option_manager_get_val_label(opt, i), "Double"), "t_res label"); + CHECK(core_option_manager_get_val_idx(opt, i, "1280x960", &idx) && idx == 2, + "t_res lookup of the last value"); + CHECK(!core_option_manager_get_val_idx(opt, i, "1280x96", &idx), + "t_res lookup matched a value it does not have"); + } + + i = find_option(opt, "t_flag"); + CHECK(i != (size_t)-1, "t_flag missing"); + if (i != (size_t)-1) + { + CHECK(opt->opts[i].index == 1, "t_flag config value not applied"); + CHECK(!strcmp(core_option_manager_get_val_label(opt, i), "ON"), + "t_flag label not sanitised"); + } + + i = find_option(opt, "t_full"); + CHECK(i != (size_t)-1, "t_full missing"); + if (i != (size_t)-1) + { + CHECK(opt->opts[i].vals->size == RETRO_NUM_CORE_OPTION_VALUES_MAX, + "t_full value count"); + CHECK(opt->opts[i].index == 5, "t_full default"); + CHECK(core_option_manager_get_val_idx(opt, i, "127", &idx) && idx == 127, + "t_full lookup of the last value"); + } + + core_option_manager_free(opt); +} + +/* Each append a v2 parse makes, made to fail in turn: the manager + * must come back NULL with nothing written past a list. The first + * failure is the first value of the first option, which a write + * through 'elems[size - 1]' would have put before the list. */ +static void t_v2_append_failures(void) +{ + unsigned n, total; + core_option_manager_t *opt; + + printf(" v2: every value/label append failing in turn\n"); + build_v2(); + write_conf(""); + + fail_append(0); + opt = core_option_manager_new(CONF_PATH, NULL, &v2_opts, false); + total = append_calls; + core_option_manager_free(opt); + CHECK(total > 0, "no appends counted"); + + for (n = 1; n <= total; n++) + { + fail_append(n); + opt = core_option_manager_new(CONF_PATH, NULL, &v2_opts, false); + if (opt) + { + printf(" FAIL: append %u of %u failed and a manager was still made\n", + n, total); + failures++; + core_option_manager_free(opt); + } + } + fail_append(0); +} + +/* --- legacy interface ---------------------------------------------- */ + +static const struct retro_variable legacy_vars[] = { + { "t_mode", "Mode; fast|accurate|slow" }, + { "t_nodesc", "; on|off" }, + { "t_gaps", "Gaps; a||b|" }, + { NULL, NULL }, +}; + +static void t_legacy(void) +{ + core_option_manager_t *opt; + size_t i, idx; + + printf(" legacy: values, description, empty tokens, config value\n"); + write_conf("t_mode = \"slow\"\nt_gaps = \"b\"\n"); + fail_append(0); + + opt = core_option_manager_new_vars(CONF_PATH, NULL, legacy_vars); + CHECK(opt != NULL, "manager not created"); + if (!opt) + return; + + i = find_option(opt, "t_mode"); + CHECK(i != (size_t)-1, "t_mode missing"); + if (i != (size_t)-1) + { + CHECK(opt->opts[i].desc && !strcmp(opt->opts[i].desc, "Mode"), "t_mode desc"); + CHECK(opt->opts[i].vals->size == 3, "t_mode value count"); + CHECK(opt->opts[i].index == 2, "t_mode config value not applied"); + CHECK(core_option_manager_get_val_idx(opt, i, "fast", &idx) && idx == 0, + "t_mode lookup"); + } + + i = find_option(opt, "t_nodesc"); + CHECK(i != (size_t)-1, "t_nodesc missing"); + if (i != (size_t)-1) + { + CHECK(opt->opts[i].desc == NULL, "t_nodesc was given a description"); + CHECK(!strcmp(core_option_manager_get_val_label(opt, i), "ON"), + "t_nodesc label not sanitised"); + } + + i = find_option(opt, "t_gaps"); + CHECK(i != (size_t)-1, "t_gaps missing"); + if (i != (size_t)-1) + { + CHECK(opt->opts[i].vals->size == 2, "t_gaps kept an empty value"); + CHECK(opt->opts[i].index == 1, "t_gaps config value not applied"); + } + + core_option_manager_free(opt); +} + +static void t_legacy_append_failures(void) +{ + unsigned n, total; + core_option_manager_t *opt; + + printf(" legacy: every label append failing in turn\n"); + write_conf(""); + + fail_append(0); + opt = core_option_manager_new_vars(CONF_PATH, NULL, legacy_vars); + total = append_calls; + core_option_manager_free(opt); + CHECK(total > 0, "no appends counted"); + + for (n = 1; n <= total; n++) + { + fail_append(n); + opt = core_option_manager_new_vars(CONF_PATH, NULL, legacy_vars); + if (opt) + { + printf(" FAIL: append %u of %u failed and a manager was still made\n", + n, total); + failures++; + core_option_manager_free(opt); + } + } + fail_append(0); +} + +int main(void) +{ + printf("core_options\n"); + t_v2(); + t_v2_append_failures(); + t_legacy(); + t_legacy_append_failures(); + remove(CONF_PATH); + + if (failures) + { + printf("%u failure(s)\n", failures); + return 1; + } + printf("all passed\n"); + return 0; +} diff --git a/samples/runahead/Makefile b/samples/runahead/Makefile index b3a0f9d606..e81a41acfb 100644 --- a/samples/runahead/Makefile +++ b/samples/runahead/Makefile @@ -57,6 +57,9 @@ preempt_mask_fragment.c: $(CORE_DIR)/runahead.c dense_cache_fragment.c: $(CORE_DIR)/runahead.c sed -n '/===== BEGIN runahead dense input cache =====/,/===== END runahead dense input cache =====/p' $< > $@ +input_list_fragment.c: $(CORE_DIR)/runahead.c + sed -n '/===== BEGIN runahead input list =====/,/===== END runahead input list =====/p' $< > $@ + # runahead_tmp_dir_private(), against real directories runahead_tmpdir_fragment.c: $(CORE_DIR)/runahead.c sed -n '/===== BEGIN runahead tmp dir check =====/,/===== END runahead tmp dir check =====/p' $< > $@ @@ -66,7 +69,7 @@ runahead_tmpdir_test: runahead_tmpdir_test.c runahead_tmpdir_fragment.c preempt_slab_fragment.c: $(CORE_DIR)/runahead.c sed -n '/===== BEGIN preempt slab =====/,/===== END preempt slab =====/p' $< > $@ -runahead_copy_test.o: runahead_copy_fragment.c preempt_mask_fragment.c dense_cache_fragment.c preempt_slab_fragment.c +runahead_copy_test.o: runahead_copy_fragment.c preempt_mask_fragment.c dense_cache_fragment.c preempt_slab_fragment.c input_list_fragment.c %.o: %.c $(CC) $(INCDIRS) $(CFLAGS) $(flags) -c -o$@ $< @@ -84,7 +87,7 @@ check: $(TARGET) runahead_tmpdir_test clean: rm -rf $(SAMPLE_OBJDIR) - rm -f $(OBJS) runahead_copy_fragment.c preempt_mask_fragment.c dense_cache_fragment.c preempt_slab_fragment.c runahead_tmpdir_fragment.c + rm -f $(OBJS) runahead_copy_fragment.c preempt_mask_fragment.c dense_cache_fragment.c preempt_slab_fragment.c input_list_fragment.c runahead_tmpdir_fragment.c rm -f $(TARGET) runahead_tmpdir_test .PHONY: clean all check diff --git a/samples/runahead/runahead_copy_test.c b/samples/runahead/runahead_copy_test.c index 5165596aed..c99b1d2e76 100644 --- a/samples/runahead/runahead_copy_test.c +++ b/samples/runahead/runahead_copy_test.c @@ -172,6 +172,21 @@ static bool runahead_tmp_dir_private(const char *dir) #include "dense_cache_fragment.c" #include "preempt_slab_fragment.c" +/* The input list, verbatim from runahead.c, with its reallocs routed + * through one that can be told to fail the Nth call. */ +static unsigned list_realloc_fail_at; +static unsigned list_realloc_calls; +static void *list_realloc(void *ptr, size_t len) +{ + list_realloc_calls++; + if (list_realloc_fail_at && list_realloc_calls == list_realloc_fail_at) + return NULL; + return realloc(ptr, len); +} +#define realloc list_realloc +#include "input_list_fragment.c" +#undef realloc + /* The dense cache: every common tuple has a slot, distinct per tuple; * everything else is refused to the list; a set then get reads back. */ static void t_dense_cache(void) @@ -482,6 +497,84 @@ static void t_tmp_dir_not_private(void) task_queue_deinit(); } +/* Values the input logging recorded read back per tuple and id: past + * the list's first growths, across a state that grows under ids it + * already holds, and with unknown tuples and ids reading 0. */ +static void t_input_list(void) +{ + unsigned port; + runahead_input_list_t *list = &runahead_input_list; + + printf(" input_list: tuples outside the dense cache read back\n"); + list_realloc_fail_at = 0; + runahead_dense = (runahead_dense_cache_t*)calloc(1, sizeof(*runahead_dense)); + + for (port = 0; port < 20; port++) + runahead_input_state_set_last(port, + RETRO_DEVICE_KEYBOARD, 0, 300, (int16_t)(1000 + port)); + runahead_input_state_set_last(0, RETRO_DEVICE_POINTER, 0, 3, 7); + runahead_input_state_set_last(0, RETRO_DEVICE_POINTER, 0, 100, 9); + runahead_input_state_set_last(1, RETRO_DEVICE_JOYPAD, 0, 4, 5); + + CHECK(list->size == 21, "list holds %u tuples, expected 21", list->size); + for (port = 0; port < 20; port++) + CHECK(input_state_get_last(port, RETRO_DEVICE_KEYBOARD, 0, 300) == (int16_t)(1000 + port), + "keyboard port %u read back wrong", port); + CHECK(input_state_get_last(0, RETRO_DEVICE_POINTER, 0, 3) == 7, + "pointer id 3 lost when the state grew for id 100"); + CHECK(input_state_get_last(0, RETRO_DEVICE_POINTER, 0, 100) == 9, "pointer id 100"); + CHECK(input_state_get_last(0, RETRO_DEVICE_POINTER, 0, 50) == 0, "unset id read nonzero"); + CHECK(input_state_get_last(0, RETRO_DEVICE_POINTER, 0, 60000) == 0, "id past the state read nonzero"); + CHECK(input_state_get_last(5, RETRO_DEVICE_POINTER, 0, 3) == 0, "unknown tuple read nonzero"); + CHECK(input_state_get_last(1, RETRO_DEVICE_JOYPAD, 0, 4) == 5, "dense tuple"); + CHECK(!runahead_input_list_find(list, 1, RETRO_DEVICE_JOYPAD, 0), + "a dense tuple went to the list"); + + runahead_input_list_free(list); + CHECK(!list->elems && !list->size && !list->cap, "free left the list set"); + free(runahead_dense); + runahead_dense = NULL; +} + +/* Every realloc the list makes, failing in turn: the write is dropped, + * nothing reads freed or unallocated memory, and later writes land. */ +static void t_input_list_oom(void) +{ + unsigned n, port, total; + runahead_input_list_t *list = &runahead_input_list; + + printf(" input_list: every realloc failing in turn\n"); + + list_realloc_fail_at = 0; + list_realloc_calls = 0; + for (port = 0; port < 12; port++) + runahead_input_state_set_last(port, + RETRO_DEVICE_POINTER, 0, port * 20, 1); + total = list_realloc_calls; + runahead_input_list_free(list); + + for (n = 1; n <= total; n++) + { + list_realloc_fail_at = n; + list_realloc_calls = 0; + for (port = 0; port < 12; port++) + { + runahead_input_state_set_last(port, + RETRO_DEVICE_POINTER, 0, port * 20, 1); + (void)input_state_get_last(port, RETRO_DEVICE_POINTER, 0, port * 20); + } + list_realloc_fail_at = 0; + for (port = 0; port < 12; port++) + { + runahead_input_state_set_last(port, + RETRO_DEVICE_POINTER, 0, port * 20, 2); + CHECK(input_state_get_last(port, RETRO_DEVICE_POINTER, 0, port * 20) == 2, + "realloc %u failed: port %u not recorded afterwards", n, port); + } + runahead_input_list_free(list); + } +} + int main(void) { /* A stray 'return false' from a status-returning function must @@ -491,6 +584,8 @@ int main(void) printf("runahead copy:\n"); t_analog_mask_bit(); t_dense_cache(); + t_input_list(); + t_input_list_oom(); t_preempt_slab(); t_lcg_names(); t_lcg_all_fail();