diff --git a/command.c b/command.c index 5ca0b21b9e..61f1554054 100644 --- a/command.c +++ b/command.c @@ -1369,6 +1369,7 @@ static bool command_event_init_core(enum rarch_core_type *data) return false; rarch_ctl(RARCH_CTL_SET_FRAME_LIMIT, NULL); + rarch_ctl(RARCH_CTL_CONTENT_RUNTIME_LOG_INIT, NULL); return true; } @@ -2215,6 +2216,13 @@ TODO: Add a setting for these tweaks */ } g_defaults.music_history = NULL; + if (g_defaults.content_runtime) + { + playlist_write_runtime_file(g_defaults.content_runtime); + playlist_free(g_defaults.content_runtime); + } + g_defaults.content_runtime = NULL; + #if defined(HAVE_FFMPEG) || defined(HAVE_MPV) if (g_defaults.video_history) { @@ -2265,6 +2273,13 @@ TODO: Add a setting for these tweaks */ settings->paths.path_content_music_history, content_history_size); + RARCH_LOG("%s: [%s].\n", + msg_hash_to_str(MSG_LOADING_HISTORY_FILE), + settings->paths.path_content_runtime); + g_defaults.content_runtime = playlist_init( + settings->paths.path_content_runtime, + content_history_size); + #if defined(HAVE_FFMPEG) || defined(HAVE_MPV) RARCH_LOG("%s: [%s].\n", msg_hash_to_str(MSG_LOADING_HISTORY_FILE), @@ -2311,6 +2326,7 @@ TODO: Add a setting for these tweaks */ case CMD_EVENT_CORE_DEINIT: { struct retro_hw_render_callback *hwr = NULL; + rarch_ctl(RARCH_CTL_CONTENT_RUNTIME_LOG_DEINIT, NULL); content_reset_savestate_backups(); hwr = video_driver_get_hw_context(); command_event_deinit_core(true); diff --git a/config.def.h b/config.def.h index bb7e77ac22..69508c4845 100644 --- a/config.def.h +++ b/config.def.h @@ -772,6 +772,9 @@ static const bool ui_companion_toggle = false; /* Only init the WIMP UI for this session if this is enabled */ static const bool desktop_menu_enable = true; +/* Keep track of how long each core+content has been running for over time */ +static const bool content_runtime_log = false; + #if defined(__QNX__) || defined(_XBOX1) || defined(_XBOX360) || defined(__CELLOS_LV2__) || (defined(__MACH__) && defined(IOS)) || defined(ANDROID) || defined(WIIU) || defined(HAVE_NEON) || defined(GEKKO) || defined(__ARM_NEON__) static enum resampler_quality audio_resampler_quality_level = RESAMPLER_QUALITY_LOWER; #elif defined(PSP) || defined(_3DS) || defined(VITA) || defined(PS2) diff --git a/configuration.c b/configuration.c index c5cfd45baf..ef9b6c3259 100644 --- a/configuration.c +++ b/configuration.c @@ -1228,6 +1228,8 @@ static struct config_path_setting *populate_settings_path(settings_t *settings, settings->paths.path_content_video_history, false, NULL, true); SETTING_PATH("content_image_history_path", settings->paths.path_content_image_history, false, NULL, true); + SETTING_PATH("content_runtime_path", + settings->paths.path_content_runtime, false, NULL, true); #ifdef HAVE_OVERLAY SETTING_PATH("input_overlay", settings->paths.path_overlay, false, NULL, true); @@ -1571,6 +1573,7 @@ static struct config_bool_setting *populate_settings_bool(settings_t *settings, #endif SETTING_BOOL("playlist_use_old_format", &settings->bools.playlist_use_old_format, true, playlist_use_old_format, false); + SETTING_BOOL("content_runtime_log", &settings->bools.content_runtime_log, true, content_runtime_log, false); *size = count; @@ -2066,6 +2069,7 @@ void config_set_defaults(void) *settings->paths.path_content_music_history = '\0'; *settings->paths.path_content_image_history = '\0'; *settings->paths.path_content_video_history = '\0'; + *settings->paths.path_content_runtime = '\0'; *settings->paths.path_cheat_settings = '\0'; *settings->paths.path_shader = '\0'; #ifndef IOS @@ -3049,6 +3053,25 @@ static bool config_load_file(const char *path, bool set_defaults, } } + if (string_is_empty(settings->paths.path_content_runtime)) + { + if (string_is_empty(settings->paths.directory_content_history)) + { + fill_pathname_resolve_relative( + settings->paths.path_content_runtime, + path_config, + file_path_str(FILE_PATH_CONTENT_RUNTIME), + sizeof(settings->paths.path_content_runtime)); + } + else + { + fill_pathname_join(settings->paths.path_content_runtime, + settings->paths.directory_content_history, + file_path_str(FILE_PATH_CONTENT_RUNTIME), + sizeof(settings->paths.path_content_runtime)); + } + } + if (!string_is_empty(settings->paths.directory_screenshot)) { if (string_is_equal(settings->paths.directory_screenshot, "default")) diff --git a/configuration.h b/configuration.h index 17286aa49a..e0fb5e9cba 100644 --- a/configuration.h +++ b/configuration.h @@ -307,6 +307,7 @@ typedef struct settings bool sustained_performance_mode; bool playlist_use_old_format; + bool content_runtime_log; } bools; struct @@ -540,6 +541,7 @@ typedef struct settings char path_content_music_history[PATH_MAX_LENGTH]; char path_content_image_history[PATH_MAX_LENGTH]; char path_content_video_history[PATH_MAX_LENGTH]; + char path_content_runtime[PATH_MAX_LENGTH]; char path_libretro_info[PATH_MAX_LENGTH]; char path_cheat_settings[PATH_MAX_LENGTH]; char path_shader[PATH_MAX_LENGTH]; diff --git a/defaults.h b/defaults.h index 117e14a39a..1a0c58f694 100644 --- a/defaults.h +++ b/defaults.h @@ -103,6 +103,7 @@ struct defaults #ifndef IS_SALAMANDER playlist_t *content_history; playlist_t *content_favorites; + playlist_t *content_runtime; #ifdef HAVE_IMAGEVIEWER playlist_t *image_history; #endif diff --git a/file_path_special.h b/file_path_special.h index 75cca3995c..092b8e7d77 100644 --- a/file_path_special.h +++ b/file_path_special.h @@ -38,6 +38,7 @@ enum file_path_enum FILE_PATH_LOG_ERROR, FILE_PATH_LOG_INFO, FILE_PATH_CONTENT_HISTORY, + FILE_PATH_CONTENT_RUNTIME, FILE_PATH_CONTENT_FAVORITES, FILE_PATH_CONTENT_MUSIC_HISTORY, FILE_PATH_CONTENT_VIDEO_HISTORY, diff --git a/file_path_str.c b/file_path_str.c index 2d9d73481d..33dd18c85a 100644 --- a/file_path_str.c +++ b/file_path_str.c @@ -200,6 +200,9 @@ const char *file_path_str(enum file_path_enum enum_idx) case FILE_PATH_CONTENT_HISTORY: str = "content_history.lpl"; break; + case FILE_PATH_CONTENT_RUNTIME: + str = "content_runtime.lpl"; + break; case FILE_PATH_CONTENT_FAVORITES: str = "content_favorites.lpl"; break; diff --git a/intl/msg_hash_lbl.h b/intl/msg_hash_lbl.h index 7d9fe1d7e8..891c3da8e7 100644 --- a/intl/msg_hash_lbl.h +++ b/intl/msg_hash_lbl.h @@ -1775,3 +1775,5 @@ MSG_HASH(MENU_ENUM_LABEL_MENU_SOUND_NOTICE, "menu_sound_notice") MSG_HASH(MENU_ENUM_LABEL_MENU_SOUND_BGM, "menu_sound_bgm") +MSG_HASH(MENU_ENUM_LABEL_CONTENT_RUNTIME_LOG, + "content_runtime_log") diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index 0e90786a03..9f608faa49 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -8194,3 +8194,11 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_SYSTEM_INFO_COREAUDIO3_SUPPORT, "CoreAudio V3 support" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_CONTENT_RUNTIME_LOG, + "Save content runtime log" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_CONTENT_RUNTIME_LOG, + "Keeps track of how long your content has been running over time." + ) diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index 0f80b087e3..afa4ec1a6b 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -516,6 +516,7 @@ default_sublabel_macro(action_bind_sublabel_menu_rgui_lock_aspect, default_sublabel_macro(action_bind_sublabel_rgui_menu_color_theme, MENU_ENUM_SUBLABEL_RGUI_MENU_COLOR_THEME) default_sublabel_macro(action_bind_sublabel_rgui_menu_theme_preset, MENU_ENUM_SUBLABEL_RGUI_MENU_THEME_PRESET) default_sublabel_macro(action_bind_sublabel_menu_rgui_thumbnail_downscaler, MENU_ENUM_SUBLABEL_MENU_RGUI_THUMBNAIL_DOWNSCALER) +default_sublabel_macro(action_bind_sublabel_content_runtime_log, MENU_ENUM_SUBLABEL_CONTENT_RUNTIME_LOG) static int action_bind_sublabel_systeminfo_controller_entry( file_list_t *list, @@ -2248,6 +2249,9 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_MENU_RGUI_THUMBNAIL_DOWNSCALER: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_menu_rgui_thumbnail_downscaler); break; + case MENU_ENUM_LABEL_CONTENT_RUNTIME_LOG: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_content_runtime_log); + break; default: case MSG_UNKNOWN: return -1; diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index 599e07544a..5fd206765f 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -5397,6 +5397,9 @@ bool menu_displaylist_ctl(enum menu_displaylist_ctl_state type, menu_displaylist menu_displaylist_parse_settings_enum(menu, info, MENU_ENUM_LABEL_SCREENSHOTS_IN_CONTENT_DIR_ENABLE, PARSE_ONLY_BOOL, false); + menu_displaylist_parse_settings_enum(menu, info, + MENU_ENUM_LABEL_CONTENT_RUNTIME_LOG, + PARSE_ONLY_BOOL, false); info->need_refresh = true; info->need_push = true; diff --git a/menu/menu_driver.c b/menu/menu_driver.c index 7e5a07bee3..165604e810 100644 --- a/menu/menu_driver.c +++ b/menu/menu_driver.c @@ -531,6 +531,7 @@ bool menu_display_libretro(bool is_idle, input_driver_set_libretro_input_blocked(); core_run(); + rarch_core_runtime_tick(); input_driver_unset_libretro_input_blocked(); return true; diff --git a/menu/menu_setting.c b/menu/menu_setting.c index 319e5084af..3319ae801c 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -4821,9 +4821,25 @@ static bool setting_append_list( &setting_get_string_representation_uint_autosave_interval; #endif + CONFIG_BOOL( + list, list_info, + &settings->bools.content_runtime_log, + MENU_ENUM_LABEL_CONTENT_RUNTIME_LOG, + MENU_ENUM_LABEL_VALUE_CONTENT_RUNTIME_LOG, + content_runtime_log, + MENU_ENUM_LABEL_VALUE_OFF, + MENU_ENUM_LABEL_VALUE_ON, + &group_info, + &subgroup_info, + parent_group, + general_write_handler, + general_read_handler, + SD_FLAG_NONE); + END_SUB_GROUP(list, list_info, parent_group); END_GROUP(list, list_info, parent_group); } + break; case SETTINGS_LIST_REWIND: START_GROUP(list, list_info, &group_info, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_REWIND_SETTINGS), parent_group); diff --git a/msg_hash.h b/msg_hash.h index 441d3fe1bd..77b0c6f35a 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -2252,6 +2252,7 @@ enum msg_hash_enums MENU_LABEL(MENU_SOUND_NOTICE), MENU_LABEL(MENU_SOUND_BGM), MENU_ENUM_LABEL_VALUE_VIDEO_DRIVER_FALLBACK, + MENU_LABEL(CONTENT_RUNTIME_LOG), MSG_LAST }; diff --git a/playlist.c b/playlist.c index 1149fbb1c8..5b50a9456f 100644 --- a/playlist.c +++ b/playlist.c @@ -45,6 +45,9 @@ struct playlist_entry char *core_name; char *db_name; char *crc32; + unsigned runtime_hours; + unsigned runtime_minutes; + unsigned runtime_seconds; }; struct content_playlist @@ -67,6 +70,8 @@ typedef struct unsigned array_depth; unsigned object_depth; char **current_entry_val; + int *current_entry_int_val; + unsigned *current_entry_uint_val; char *current_meta_string; bool in_items; } JSONContext; @@ -125,6 +130,27 @@ void playlist_get_index(playlist_t *playlist, *crc32 = playlist->entries[idx].crc32; } +void playlist_get_runtime_index(playlist_t *playlist, + size_t idx, + const char **path, const char **core_path, + unsigned *runtime_hours, unsigned *runtime_minutes, + unsigned *runtime_seconds) +{ + if (!playlist) + return; + + if (path) + *path = playlist->entries[idx].path; + if (core_path) + *core_path = playlist->entries[idx].core_path; + if (runtime_hours) + *runtime_hours = playlist->entries[idx].runtime_hours; + if (runtime_minutes) + *runtime_minutes = playlist->entries[idx].runtime_minutes; + if (runtime_seconds) + *runtime_seconds = playlist->entries[idx].runtime_seconds; +} + /** * playlist_delete_index: * @playlist : Playlist handle. @@ -223,6 +249,9 @@ static void playlist_free_entry(struct playlist_entry *entry) entry->core_name = NULL; entry->db_name = NULL; entry->crc32 = NULL; + entry->runtime_hours = 0; + entry->runtime_minutes = 0; + entry->runtime_seconds = 0; } void playlist_update(playlist_t *playlist, size_t idx, @@ -288,6 +317,146 @@ void playlist_update(playlist_t *playlist, size_t idx, } } +void playlist_update_runtime(playlist_t *playlist, size_t idx, + const char *path, const char *core_path, + unsigned runtime_hours, unsigned runtime_minutes, + unsigned runtime_seconds) +{ + struct playlist_entry *entry = NULL; + + if (!playlist || idx > playlist->size) + return; + + entry = &playlist->entries[idx]; + + if (path && (path != entry->path)) + { + if (entry->path != NULL) + free(entry->path); + entry->path = strdup(path); + playlist->modified = true; + } + + if (core_path && (core_path != entry->core_path)) + { + if (entry->core_path != NULL) + free(entry->core_path); + entry->core_path = NULL; + entry->core_path = strdup(core_path); + playlist->modified = true; + } + + if (runtime_hours != entry->runtime_hours) + { + entry->runtime_hours = runtime_hours; + playlist->modified = true; + } + + if (runtime_minutes != entry->runtime_minutes) + { + entry->runtime_minutes = runtime_minutes; + playlist->modified = true; + } + + if (runtime_seconds != entry->runtime_seconds) + { + entry->runtime_seconds = runtime_seconds; + playlist->modified = true; + } +} + +bool playlist_push_runtime(playlist_t *playlist, + const char *path, const char *core_path, + unsigned runtime_hours, unsigned runtime_minutes, + unsigned runtime_seconds) +{ + size_t i; + bool core_path_empty = string_is_empty(core_path); + + if (core_path_empty) + { + RARCH_ERR("cannot push NULL or empty core name into the playlist.\n"); + return false; + } + + if (string_is_empty(path)) + path = NULL; + + if (!playlist) + return false; + + for (i = 0; i < playlist->size; i++) + { + struct playlist_entry tmp; + bool equal_path; + + equal_path = (!path && !playlist->entries[i].path) || + (path && playlist->entries[i].path && +#ifdef _WIN32 + /*prevent duplicates on case-insensitive operating systems*/ + string_is_equal_noncase(path,playlist->entries[i].path) +#else + string_is_equal(path,playlist->entries[i].path) +#endif + ); + + /* Core name can have changed while still being the same core. + * Differentiate based on the core path only. */ + if (!equal_path) + continue; + + if (!string_is_equal(playlist->entries[i].core_path, core_path)) + continue; + + /* If top entry, we don't want to push a new entry since + * the top and the entry to be pushed are the same. */ + if (i == 0) + return false; + + /* Seen it before, bump to top. */ + tmp = playlist->entries[i]; + memmove(playlist->entries + 1, playlist->entries, + i * sizeof(struct playlist_entry)); + playlist->entries[0] = tmp; + + goto success; + } + + if (playlist->size == playlist->cap) + { + struct playlist_entry *entry = &playlist->entries[playlist->cap - 1]; + + if (entry) + playlist_free_entry(entry); + playlist->size--; + } + + if (playlist->entries) + { + memmove(playlist->entries + 1, playlist->entries, + (playlist->cap - 1) * sizeof(struct playlist_entry)); + + playlist->entries[0].path = NULL; + playlist->entries[0].core_path = NULL; + + if (!string_is_empty(path)) + playlist->entries[0].path = strdup(path); + if (!string_is_empty(core_path)) + playlist->entries[0].core_path = strdup(core_path); + + playlist->entries[0].runtime_hours = runtime_hours; + playlist->entries[0].runtime_minutes = runtime_minutes; + playlist->entries[0].runtime_seconds = runtime_seconds; + } + + playlist->size++; + +success: + playlist->modified = true; + + return true; +} + /** * playlist_push: * @playlist : Playlist handle. @@ -380,12 +549,15 @@ bool playlist_push(playlist_t *playlist, memmove(playlist->entries + 1, playlist->entries, (playlist->cap - 1) * sizeof(struct playlist_entry)); - playlist->entries[0].path = NULL; - playlist->entries[0].label = NULL; - playlist->entries[0].core_path = NULL; - playlist->entries[0].core_name = NULL; - playlist->entries[0].db_name = NULL; - playlist->entries[0].crc32 = NULL; + playlist->entries[0].path = NULL; + playlist->entries[0].label = NULL; + playlist->entries[0].core_path = NULL; + playlist->entries[0].core_name = NULL; + playlist->entries[0].db_name = NULL; + playlist->entries[0].crc32 = NULL; + playlist->entries[0].runtime_hours = 0; + playlist->entries[0].runtime_minutes = 0; + playlist->entries[0].runtime_seconds = 0; if (!string_is_empty(path)) playlist->entries[0].path = strdup(path); if (!string_is_empty(label)) @@ -435,6 +607,136 @@ static void JSONLogError(JSONContext *pCtx) } } +void playlist_write_runtime_file(playlist_t *playlist) +{ + size_t i; + RFILE *file = NULL; + settings_t *settings = config_get_ptr(); + JSONContext context = {0}; + + if (!playlist || !playlist->modified) + return; + + file = filestream_open(playlist->conf_path, + RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); + + if (!file) + { + RARCH_ERR("Failed to write to playlist file: %s\n", playlist->conf_path); + return; + } + + context.writer = JSON_Writer_Create(NULL); + context.file = file; + + if (!context.writer) + { + RARCH_ERR("Failed to create JSON writer\n"); + goto end; + } + + JSON_Writer_SetOutputEncoding(context.writer, JSON_UTF8); + JSON_Writer_SetOutputHandler(context.writer, &JSONOutputHandler); + JSON_Writer_SetUserData(context.writer, &context); + + JSON_Writer_WriteStartObject(context.writer); + JSON_Writer_WriteNewLine(context.writer); + JSON_Writer_WriteSpace(context.writer, 2); + JSON_Writer_WriteString(context.writer, "version", strlen("version"), JSON_UTF8); + JSON_Writer_WriteColon(context.writer); + JSON_Writer_WriteSpace(context.writer, 1); + JSON_Writer_WriteString(context.writer, "1.0", strlen("1.0"), JSON_UTF8); + JSON_Writer_WriteComma(context.writer); + JSON_Writer_WriteNewLine(context.writer); + JSON_Writer_WriteSpace(context.writer, 2); + JSON_Writer_WriteString(context.writer, "items", strlen("items"), JSON_UTF8); + JSON_Writer_WriteColon(context.writer); + JSON_Writer_WriteSpace(context.writer, 1); + JSON_Writer_WriteStartArray(context.writer); + JSON_Writer_WriteNewLine(context.writer); + + for (i = 0; i < playlist->size; i++) + { + JSON_Writer_WriteSpace(context.writer, 4); + JSON_Writer_WriteStartObject(context.writer); + + JSON_Writer_WriteNewLine(context.writer); + JSON_Writer_WriteSpace(context.writer, 6); + JSON_Writer_WriteString(context.writer, "path", strlen("path"), JSON_UTF8); + JSON_Writer_WriteColon(context.writer); + JSON_Writer_WriteSpace(context.writer, 1); + JSON_Writer_WriteString(context.writer, playlist->entries[i].path ? playlist->entries[i].path : "", playlist->entries[i].path ? strlen(playlist->entries[i].path) : 0, JSON_UTF8); + JSON_Writer_WriteComma(context.writer); + + JSON_Writer_WriteNewLine(context.writer); + JSON_Writer_WriteSpace(context.writer, 6); + JSON_Writer_WriteString(context.writer, "core_path", strlen("core_path"), JSON_UTF8); + JSON_Writer_WriteColon(context.writer); + JSON_Writer_WriteSpace(context.writer, 1); + JSON_Writer_WriteString(context.writer, playlist->entries[i].core_path, strlen(playlist->entries[i].core_path), JSON_UTF8); + JSON_Writer_WriteComma(context.writer); + JSON_Writer_WriteNewLine(context.writer); + + { + char tmp[32] = {0}; + + snprintf(tmp, sizeof(tmp), "%u", playlist->entries[i].runtime_hours); + + JSON_Writer_WriteSpace(context.writer, 6); + JSON_Writer_WriteString(context.writer, "runtime_hours", strlen("runtime_hours"), JSON_UTF8); + JSON_Writer_WriteColon(context.writer); + JSON_Writer_WriteSpace(context.writer, 1); + JSON_Writer_WriteNumber(context.writer, tmp, strlen(tmp), JSON_UTF8); + JSON_Writer_WriteComma(context.writer); + JSON_Writer_WriteNewLine(context.writer); + + memset(tmp, 0, sizeof(tmp)); + + snprintf(tmp, sizeof(tmp), "%u", playlist->entries[i].runtime_minutes); + + JSON_Writer_WriteSpace(context.writer, 6); + JSON_Writer_WriteString(context.writer, "runtime_minutes", strlen("runtime_minutes"), JSON_UTF8); + JSON_Writer_WriteColon(context.writer); + JSON_Writer_WriteSpace(context.writer, 1); + JSON_Writer_WriteNumber(context.writer, tmp, strlen(tmp), JSON_UTF8); + JSON_Writer_WriteComma(context.writer); + JSON_Writer_WriteNewLine(context.writer); + + memset(tmp, 0, sizeof(tmp)); + + snprintf(tmp, sizeof(tmp), "%u", playlist->entries[i].runtime_seconds); + + JSON_Writer_WriteSpace(context.writer, 6); + JSON_Writer_WriteString(context.writer, "runtime_seconds", strlen("runtime_seconds"), JSON_UTF8); + JSON_Writer_WriteColon(context.writer); + JSON_Writer_WriteSpace(context.writer, 1); + JSON_Writer_WriteNumber(context.writer, tmp, strlen(tmp), JSON_UTF8); + JSON_Writer_WriteNewLine(context.writer); + } + + JSON_Writer_WriteSpace(context.writer, 4); + JSON_Writer_WriteEndObject(context.writer); + + if (i < playlist->size - 1) + JSON_Writer_WriteComma(context.writer); + + JSON_Writer_WriteNewLine(context.writer); + } + + JSON_Writer_WriteSpace(context.writer, 2); + JSON_Writer_WriteEndArray(context.writer); + JSON_Writer_WriteNewLine(context.writer); + JSON_Writer_WriteEndObject(context.writer); + JSON_Writer_WriteNewLine(context.writer); + JSON_Writer_Free(context.writer); + + playlist->modified = false; + + RARCH_LOG("Written to playlist file: %s\n", playlist->conf_path); +end: + filestream_close(file); +} + void playlist_write_file(playlist_t *playlist) { size_t i; @@ -735,6 +1037,9 @@ static JSON_Parser_HandlerResult JSONStringHandler(JSON_Parser parser, char *pVa { if (pCtx->current_entry_val && length && !string_is_empty(pValue)) { + if (*pCtx->current_entry_val) + free(*pCtx->current_entry_val); + *pCtx->current_entry_val = strdup(pValue); } else @@ -753,6 +1058,7 @@ static JSON_Parser_HandlerResult JSONStringHandler(JSON_Parser parser, char *pVa /*RARCH_LOG("found meta: %s = %s\n", pCtx->current_meta_string, pValue);*/ free(pCtx->current_meta_string); + pCtx->current_meta_string = NULL; } } } @@ -762,6 +1068,50 @@ static JSON_Parser_HandlerResult JSONStringHandler(JSON_Parser parser, char *pVa return JSON_Parser_Continue; } +static JSON_Parser_HandlerResult JSONNumberHandler(JSON_Parser parser, char *pValue, size_t length, JSON_StringAttributes attributes) +{ + JSONContext *pCtx = (JSONContext*)JSON_Parser_GetUserData(parser); + (void)attributes; /* unused */ + + if (pCtx->in_items && pCtx->object_depth == 2) + { + if (pCtx->array_depth == 1) + { + if (pCtx->current_entry_int_val && length && !string_is_empty(pValue)) + { + *pCtx->current_entry_int_val = strtoul(pValue, NULL, 10); + } + else if (pCtx->current_entry_uint_val && length && !string_is_empty(pValue)) + { + *pCtx->current_entry_uint_val = strtoul(pValue, NULL, 10); + } + else + { + /* must be a value for an unknown member we aren't tracking, skip it */ + } + } + } + else if (pCtx->object_depth == 1) + { + if (pCtx->array_depth == 0) + { + if (pCtx->current_meta_string && length && !string_is_empty(pValue)) + { + /* handle any top-level playlist metadata here */ + /*RARCH_LOG("found meta: %s = %s\n", pCtx->current_meta_string, pValue);*/ + + free(pCtx->current_meta_string); + pCtx->current_meta_string = NULL; + } + } + } + + pCtx->current_entry_int_val = NULL; + pCtx->current_entry_uint_val = NULL; + + return JSON_Parser_Continue; +} + static JSON_Parser_HandlerResult JSONObjectMemberHandler(JSON_Parser parser, char *pValue, size_t length, JSON_StringAttributes attributes) { JSONContext *pCtx = (JSONContext*)JSON_Parser_GetUserData(parser); @@ -792,6 +1142,12 @@ static JSON_Parser_HandlerResult JSONObjectMemberHandler(JSON_Parser parser, cha pCtx->current_entry_val = &pCtx->current_entry->crc32; else if (string_is_equal(pValue, "db_name")) pCtx->current_entry_val = &pCtx->current_entry->db_name; + else if (string_is_equal(pValue, "runtime_hours")) + pCtx->current_entry_uint_val = &pCtx->current_entry->runtime_hours; + else if (string_is_equal(pValue, "runtime_minutes")) + pCtx->current_entry_uint_val = &pCtx->current_entry->runtime_minutes; + else if (string_is_equal(pValue, "runtime_seconds")) + pCtx->current_entry_uint_val = &pCtx->current_entry->runtime_seconds; else { /* ignore unknown members */ @@ -805,6 +1161,9 @@ static JSON_Parser_HandlerResult JSONObjectMemberHandler(JSON_Parser parser, cha { if (length) { + if (pCtx->current_meta_string) + free(pCtx->current_meta_string); + pCtx->current_meta_string = strdup(pValue); } } @@ -886,10 +1245,10 @@ static bool playlist_read_file( /*JSON_Parser_SetNullHandler(context.parser, &JSONNullHandler); JSON_Parser_SetBooleanHandler(context.parser, &JSONBooleanHandler); - JSON_Parser_SetNumberHandler(context.parser, &JSONNumberHandler); JSON_Parser_SetSpecialNumberHandler(context.parser, &JSONSpecialNumberHandler); JSON_Parser_SetArrayItemHandler(context.parser, &JSONArrayItemHandler);*/ + JSON_Parser_SetNumberHandler(context.parser, &JSONNumberHandler); JSON_Parser_SetStringHandler(context.parser, &JSONStringHandler); JSON_Parser_SetStartObjectHandler(context.parser, &JSONStartObjectHandler); JSON_Parser_SetEndObjectHandler(context.parser, &JSONEndObjectHandler); @@ -928,6 +1287,9 @@ static bool playlist_read_file( } JSON_Parser_Free(context.parser); + + if (context.current_meta_string) + free(context.current_meta_string); } else { diff --git a/playlist.h b/playlist.h index 308297142e..62e3c2c607 100644 --- a/playlist.h +++ b/playlist.h @@ -79,6 +79,12 @@ void playlist_get_index(playlist_t *playlist, const char **db_name, const char **crc32); +void playlist_get_runtime_index(playlist_t *playlist, + size_t idx, + const char **path, const char **core_path, + unsigned *runtime_hours, unsigned *runtime_minutes, + unsigned *runtime_seconds); + /** * playlist_delete_index: * @playlist : Playlist handle. @@ -104,12 +110,22 @@ bool playlist_push(playlist_t *playlist, const char *crc32, const char *db_name); +bool playlist_push_runtime(playlist_t *playlist, + const char *path, const char *core_path, + unsigned runtime_hours, unsigned runtime_minutes, + unsigned runtime_seconds); + void playlist_update(playlist_t *playlist, size_t idx, const char *path, const char *label, const char *core_path, const char *core_name, const char *crc32, const char *db_name); +void playlist_update_runtime(playlist_t *playlist, size_t idx, + const char *path, const char *core_path, + unsigned runtime_hours, unsigned runtime_minutes, + unsigned runtime_seconds); + void playlist_get_index_by_path(playlist_t *playlist, const char *search_path, char **path, char **label, @@ -127,6 +143,8 @@ uint32_t playlist_get_size(playlist_t *playlist); void playlist_write_file(playlist_t *playlist); +void playlist_write_runtime_file(playlist_t *playlist); + void playlist_qsort(playlist_t *playlist); void playlist_free_cached(void); diff --git a/retroarch.c b/retroarch.c index b0e60f4ed2..7c0fe59bfb 100644 --- a/retroarch.c +++ b/retroarch.c @@ -102,6 +102,8 @@ #include "file_path_special.h" #include "ui/ui_companion_driver.h" #include "verbosity.h" +#include "defaults.h" +#include "playlist.h" #include "frontend/frontend_driver.h" #include "audio/audio_driver.h" @@ -263,11 +265,20 @@ static unsigned fastforward_after_frames = 0; static retro_usec_t runloop_frame_time_last = 0; static retro_time_t frame_limit_minimum_time = 0.0; static retro_time_t frame_limit_last_time = 0.0; +static retro_time_t libretro_core_runtime_usec = 0; extern bool input_driver_flushing_input; static char launch_arguments[4096]; +void rarch_core_runtime_tick(void) +{ + struct retro_system_av_info *av_info = video_viewport_get_system_av_info(); + + if (av_info && av_info->timing.fps) + libretro_core_runtime_usec += (1.0 / av_info->timing.fps) * 1000 * 1000; +} + #ifdef HAVE_THREADS void runloop_msg_queue_lock(void) { @@ -1809,6 +1820,82 @@ bool rarch_ctl(enum rarch_ctl_state state, void *data) / (av_info->timing.fps * fastforward_ratio)); } break; + case RARCH_CTL_CONTENT_RUNTIME_LOG_INIT: + libretro_core_runtime_usec = 0; + break; + case RARCH_CTL_CONTENT_RUNTIME_LOG_DEINIT: + { + settings_t *settings = config_get_ptr(); + unsigned seconds = libretro_core_runtime_usec / 1000 / 1000; + unsigned minutes = seconds / 60; + unsigned hours = minutes / 60; + char log[PATH_MAX_LENGTH] = {0}; + size_t pos = 0; + + seconds -= minutes * 60; + seconds -= hours * 60 *60; + + pos = strlcpy(log, "Content ran for a total of", sizeof(log)); + + if (hours > 0) + pos += snprintf(log + pos, sizeof(log) - pos, ", %d hours", hours); + + if (minutes > 0) + pos += snprintf(log + pos, sizeof(log) - pos, ", %d minutes", minutes); + + pos += snprintf(log + pos, sizeof(log) - pos, ", %d seconds", seconds); + + if (pos < sizeof(log) - 2) + { + log[pos++] = '.'; + log[pos++] = '\n'; + } + + RARCH_LOG(log); + + if (settings->bools.content_runtime_log && g_defaults.content_runtime) + { + const char *path = path_get(RARCH_PATH_CONTENT); + const char *core_path = path_get(RARCH_PATH_CORE); + + if (!string_is_empty(path) && !string_is_empty(core_path)) + { + playlist_push_runtime(g_defaults.content_runtime, path_get(RARCH_PATH_CONTENT), path_get(RARCH_PATH_CORE), 0, 0, 0); + + /* if entry already existed, the runtime won't be updated, so manually update it again */ + if (playlist_get_size(g_defaults.content_runtime) > 0) + { + unsigned runtime_hours = 0; + unsigned runtime_minutes = 0; + unsigned runtime_seconds = 0; + + playlist_get_runtime_index(g_defaults.content_runtime, 0, NULL, NULL, &runtime_hours, &runtime_minutes, &runtime_seconds); + + runtime_seconds += seconds; + + if (runtime_seconds >= 60) + { + runtime_minutes += runtime_seconds / 60; + runtime_seconds -= runtime_minutes * 60; + } + + runtime_minutes += minutes; + + if (runtime_minutes >= 60) + { + runtime_hours += runtime_minutes / 60; + runtime_minutes -= runtime_hours * 60; + } + + runtime_hours += hours; + + playlist_update_runtime(g_defaults.content_runtime, 0, path_get(RARCH_PATH_CONTENT), path_get(RARCH_PATH_CORE), runtime_hours, runtime_minutes, runtime_seconds); + } + } + } + + break; + } case RARCH_CTL_GET_PERFCNT: { bool **perfcnt = (bool**)data; @@ -3681,10 +3768,16 @@ int runloop_iterate(unsigned *sleep_ms) ) run_ahead(run_ahead_num_frames, settings->bools.run_ahead_secondary_instance); else + { core_run(); + rarch_core_runtime_tick(); + } } #else - core_run(); + { + core_run(); + rarch_core_runtime_tick(); + } #endif #ifdef HAVE_CHEEVOS diff --git a/retroarch.cfg b/retroarch.cfg index d83ba0d399..ebb00ba97c 100644 --- a/retroarch.cfg +++ b/retroarch.cfg @@ -897,3 +897,6 @@ # File format to use when writing playlists to disk # playlist_use_old_format = false + +# Keep track of how long each core+content has been running for over time +# content_runtime_log = false diff --git a/retroarch.h b/retroarch.h index 31c82b8ca5..1727ef6ac7 100644 --- a/retroarch.h +++ b/retroarch.h @@ -183,7 +183,10 @@ enum rarch_ctl_state /* HTTP server */ RARCH_CTL_HTTPSERVER_INIT, - RARCH_CTL_HTTPSERVER_DESTROY + RARCH_CTL_HTTPSERVER_DESTROY, + + RARCH_CTL_CONTENT_RUNTIME_LOG_INIT, + RARCH_CTL_CONTENT_RUNTIME_LOG_DEINIT }; enum rarch_capabilities @@ -417,6 +420,8 @@ void runloop_msg_queue_unlock(void); void rarch_force_video_driver_fallback(const char *driver); +void rarch_core_runtime_tick(void); + RETRO_END_DECLS #endif