diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a843b572..1c9f0c99 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -112,12 +112,12 @@ jobs: - name: Build shell: cmd run: | - cmake --build _Build/windows --target launcher audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests --parallel + cmake --build _Build/windows --target launcher audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests --parallel - name: Test shell: cmd run: | - ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|ime_dialog|virtual_memory_allocation)$" + ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|ime_dialog|lru_cache|virtual_memory_allocation)$" - name: Install shell: cmd @@ -198,14 +198,14 @@ jobs: shell: bash run: | cmake --build _Build/macos \ - --target launcher audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests \ + --target launcher audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \ --parallel - name: Test shell: bash run: | ctest --test-dir _Build/macos --output-on-failure \ - -R '^(audio_out2_port|ime_dialog|virtual_memory_allocation)$' + -R '^(audio_out2_port|ime_dialog|lru_cache|virtual_memory_allocation)$' - name: Install shell: bash @@ -379,14 +379,14 @@ jobs: run: | cmake --build _Build/linux \ --target launcher page_manager_tests memory_tracker_tests \ - audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests \ + audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \ --parallel - name: Test shell: bash run: | ctest --test-dir _Build/linux --output-on-failure \ - -R '^(audio_out2_port|ime_dialog|page_manager|memory_tracker|virtual_memory_allocation)$' + -R '^(audio_out2_port|ime_dialog|lru_cache|page_manager|memory_tracker|virtual_memory_allocation)$' - name: Install shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index 716b9d31..9952bae3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -411,6 +411,11 @@ add_executable(bit_array_tests EXCLUDE_FROM_ALL ) target_include_directories(bit_array_tests PRIVATE ${inc_headers}) +add_executable(lru_cache_tests EXCLUDE_FROM_ALL + "${KYTY_TESTS_DIR}/LruCacheTests.cpp" +) +target_include_directories(lru_cache_tests PRIVATE ${inc_headers}) + add_executable(ime_dialog_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/ImeDialogTests.cpp" "${KYTY_SOURCE_DIR}/libs/imeCommon.cpp" @@ -555,6 +560,7 @@ if(BUILD_TESTING) add_test(NAME memory_tracker COMMAND $) add_test(NAME page_manager COMMAND $) add_test(NAME bit_array COMMAND $) + add_test(NAME lru_cache COMMAND $) add_test(NAME shader_vertex_metadata COMMAND $) add_test(NAME resource_materialization COMMAND $) add_test(NAME resource_tracking COMMAND $) @@ -619,6 +625,7 @@ if(BUILD_TESTING) memory_tracker_tests page_manager_tests bit_array_tests + lru_cache_tests shader_vertex_metadata_tests resource_materialization_tests resource_tracking_tests diff --git a/src/launcher/include/updateChecker.h b/src/launcher/include/updateChecker.h index 86d738a0..e94414d5 100644 --- a/src/launcher/include/updateChecker.h +++ b/src/launcher/include/updateChecker.h @@ -3,6 +3,8 @@ #include #include +#include +#include class QByteArray; class QWidget; @@ -29,6 +31,11 @@ private: QWidget* m_parent = nullptr; QNetworkAccessManager m_network; bool m_checking_updates = false; + + // Answer from the primary update feed, kept while the GitHub fallback feed is + // being consulted, so it can still be shown if the fallback request fails. + QString m_primary_tag; + QUrl m_primary_page_url; }; #endif // UPDATE_CHECKER_H diff --git a/src/launcher/src/updateChecker.cpp b/src/launcher/src/updateChecker.cpp index 565946b3..f0ef2e17 100644 --- a/src/launcher/src/updateChecker.cpp +++ b/src/launcher/src/updateChecker.cpp @@ -84,12 +84,30 @@ void UpdateChecker::FetchUpdateInfo(const char* url, bool fallback, bool manual) } reply->deleteLater(); - if (!fallback && - (!info.error.isEmpty() || info.tag != QString::fromLatin1(KYTY_RELEASE_TAG))) { - FetchUpdateInfo(FALLBACK_FEED_URL, true, manual); - return; + if (!fallback) { + const bool up_to_date = + info.error.isEmpty() && info.tag == QString::fromLatin1(KYTY_RELEASE_TAG); + if (!up_to_date) { + if (info.error.isEmpty()) { + // The primary feed is reachable but disagrees with the build tag + // (e.g. a stale mirror); remember its answer in case the fallback + // request also fails. + m_primary_tag = info.tag; + m_primary_page_url = info.page_url; + } + FetchUpdateInfo(FALLBACK_FEED_URL, true, manual); + return; + } } + if (fallback && !info.error.isEmpty() && !m_primary_tag.isEmpty()) { + // The fallback feed failed, but the primary feed already returned a + // usable answer; prefer it over reporting a network error. + info = {m_primary_tag, m_primary_page_url, {}}; + } + m_primary_tag.clear(); + m_primary_page_url.clear(); + m_checking_updates = false; emit CheckingChanged(false); ShowUpdateResult(info, manual); diff --git a/src/main.cpp b/src/main.cpp index 7f26d079..a769a6f3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -133,6 +133,26 @@ static bool ParseConsoleLanguage(const std::string& value, uint32_t& out) { return true; } +static bool ParseUint32(const std::string& value, uint32_t& out) { + uint32_t number = 0; + auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), number); + if (error != std::errc {} || end != value.data() + value.size()) { + return false; + } + out = number; + return true; +} + +static bool ParseInt32(const std::string& value, int32_t& out) { + int32_t number = 0; + auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), number); + if (error != std::errc {} || end != value.data() + value.size()) { + return false; + } + out = number; + return true; +} + static bool ParseUserId(const std::string& value, int32_t& out) { int32_t user_id = 0; auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), user_id); @@ -246,9 +266,17 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he } options.game_patch = path; } else if (arg == "--screen-width") { - options.config.screen_width = static_cast(Common::ToInt32(value)); + if (!ParseUint32(value, options.config.screen_width) || + options.config.screen_width == 0) { + ::printf("invalid screen width: %s\n", value.c_str()); + return false; + } } else if (arg == "--screen-height") { - options.config.screen_height = static_cast(Common::ToInt32(value)); + if (!ParseUint32(value, options.config.screen_height) || + options.config.screen_height == 0) { + ::printf("invalid screen height: %s\n", value.c_str()); + return false; + } } else if (arg == "--user-name") { if (value.empty() || value.size() > Config::MAX_USER_NAME_LENGTH) { ::printf("invalid user name: must contain 1-%zu bytes\n", @@ -269,11 +297,15 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he return false; } } else if (arg == "--gpu") { - options.config.gpu_index = Common::ToInt32(value); + if (!ParseInt32(value, options.config.gpu_index)) { + ::printf("invalid gpu index: %s\n", value.c_str()); + return false; + } } else if (arg == "--vblank-frequency") { - const int32_t vblank_frequency = Common::ToInt32(value); - options.config.vblank_frequency = - static_cast(vblank_frequency < 0 ? 0 : vblank_frequency); + if (!ParseUint32(value, options.config.vblank_frequency)) { + ::printf("invalid vblank frequency: %s\n", value.c_str()); + return false; + } } else if (arg == "--console-language") { if (!ParseConsoleLanguage(value, options.config.console_language)) { ::printf("invalid console language: %s\n", value.c_str()); diff --git a/tests/LruCacheTests.cpp b/tests/LruCacheTests.cpp new file mode 100644 index 00000000..d6ac94bd --- /dev/null +++ b/tests/LruCacheTests.cpp @@ -0,0 +1,156 @@ +#include "common/lruCache.h" + +#include +#include +#include +#include +#include + +namespace { + +void Check(bool value, const char *message) { + if (!value) { + std::fprintf(stderr, "LruCacheTests: failed: %s\n", message); + std::abort(); + } +} + +// Collect the objects reachable through ForEachItemBelow(), in visit order. +std::vector Collect(Common::LeastRecentlyUsedCache &cache, + uint64_t tick) { + std::vector visited; + cache.ForEachItemBelow(tick, [&](const std::string &object) { visited.push_back(object); }); + return visited; +} + +void TestInsertAndVisitOrder() { + Common::LeastRecentlyUsedCache cache; + const auto a = cache.Insert("a", 10); + const auto b = cache.Insert("b", 20); + const auto c = cache.Insert("c", 30); + + Check(a != b && b != c && a != c, "Insert returned duplicate ids"); + + const auto all = Collect(cache, UINT64_MAX); + Check(all.size() == 3 && all[0] == "a" && all[1] == "b" && all[2] == "c", + "inserted items are not visited in insertion order"); + + const auto below = Collect(cache, 20); + Check(below.size() == 2 && below[0] == "a" && below[1] == "b", + "ForEachItemBelow visited an item above the tick"); + + const auto none = Collect(cache, 5); + Check(none.empty(), "ForEachItemBelow visited items below an empty tick"); +} + +void TestEarlyExitCallback() { + Common::LeastRecentlyUsedCache cache; + (void)cache.Insert("a", 10); + (void)cache.Insert("b", 20); + (void)cache.Insert("c", 30); + + std::vector visited; + cache.ForEachItemBelow(UINT64_MAX, [&](const std::string &object) { + visited.push_back(object); + return true; // stop after the first item + }); + Check(visited.size() == 1 && visited[0] == "a", "boolean callback did not stop the walk"); + + std::vector all_visited; + cache.ForEachItemBelow(UINT64_MAX, [&](const std::string &object) { + all_visited.push_back(object); + return false; + }); + Check(all_visited.size() == 3, "false-returning callback did not visit every item"); +} + +void TestTouchReordersAndSkips() { + Common::LeastRecentlyUsedCache cache; + const auto a = cache.Insert("a", 10); + const auto b = cache.Insert("b", 20); + const auto c = cache.Insert("c", 30); + + // Touching the last item must not change the order. + cache.Touch(c, 35); + auto order = Collect(cache, UINT64_MAX); + Check(order.size() == 3 && order[0] == "a" && order[1] == "b" && order[2] == "c", + "touching the newest item changed the order"); + + // An older or equal tick must be ignored. + cache.Touch(a, 10); + cache.Touch(a, 5); + order = Collect(cache, UINT64_MAX); + Check(order[0] == "a" && order[1] == "b" && order[2] == "c", + "an ignored touch changed the order"); + + // Touching the oldest item moves it to the newest end. + cache.Touch(a, 40); + order = Collect(cache, UINT64_MAX); + Check(order.size() == 3 && order[0] == "b" && order[1] == "c" && order[2] == "a", + "touching an item did not move it to the newest end"); + + // The touched item now carries its new tick for tick filtering + // (a=40, b=20, c=35 after the touches above). A cutoff of 30 detects + // a stale c tick: c is visited only if Touch(c, 35) updated it. + const auto below = Collect(cache, 30); + Check(below.size() == 1 && below[0] == "b", + "touched item did not adopt its new tick"); + + // Touch the middle item. + cache.Touch(b, 45); + order = Collect(cache, UINT64_MAX); + Check(order.size() == 3 && order[0] == "c" && order[1] == "a" && order[2] == "b", + "touching a middle item did not move it to the end"); +} + +void TestFreeAndIdReuse() { + Common::LeastRecentlyUsedCache cache; + const auto a = cache.Insert("a", 10); + const auto b = cache.Insert("b", 20); + const auto c = cache.Insert("c", 30); + + // Free the first and the last items. + cache.Free(a); + cache.Free(c); + auto order = Collect(cache, UINT64_MAX); + Check(order.size() == 1 && order[0] == "b", "Free left a freed item in the walk"); + + // Freed ids must be reused by later inserts. + const auto d = cache.Insert("d", 40); + Check(d == a || d == c, "Insert did not reuse a freed id"); + const auto e = cache.Insert("e", 50); + Check(e != d && (e == a || e == c), "Insert reused the same freed id twice"); + + order = Collect(cache, UINT64_MAX); + Check(order.size() == 3 && order[0] == "b" && order[1] == "d" && order[2] == "e", + "reused ids are not visited in tick order"); + + // Touching a reused id must not disturb the other items. + cache.Touch(d, 60); + order = Collect(cache, UINT64_MAX); + Check(order.size() == 3 && order[0] == "b" && order[1] == "e" && order[2] == "d", + "touching a reused id changed the wrong items"); + + // Free everything, then rebuild. + cache.Free(b); + cache.Free(d); + cache.Free(e); + Check(Collect(cache, UINT64_MAX).empty(), "freeing every item left items in the walk"); + + const auto again = cache.Insert("again", 70); + const auto order2 = Collect(cache, UINT64_MAX); + Check(order2.size() == 1 && order2[0] == "again", "insert after a full free failed"); + cache.Touch(again, 70); + Check(Collect(cache, 70).size() == 1, "touching a rebuilt item failed"); +} + +} // namespace + +int main() { + TestInsertAndVisitOrder(); + TestEarlyExitCallback(); + TestTouchReordersAndSkips(); + TestFreeAndIdReuse(); + std::puts("LruCacheTests: all cases passed"); + return 0; +}