From f356c1eafd8e84af795267d98c0dd2ee25df0567 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Tue, 13 Oct 2026 17:38:50 -0500 Subject: [PATCH] Music screen: large playlists no longer empty it, and two logs say why things fail Music screen: large playlists no longer empty it, and two logs say why things fail Music screen - The screen's list was turned down whole, and came up empty (the game's own Featured row too), once it held more than 1,024 songs: a few large YouTube Music, Spotify or local playlists went over that. The most is now 2,000 songs and 16,384 places in playlists (music_ui_most_songs, music_ui_most_places); 2,000 because the screen's own table of songs is read only up to 2,048. - The rows of the player's playlists are counted as they are registered and held under that: with no room left, the rest of a playlist is left out of its list (it still plays) and the screen is never empty for it. - A row whose "artist - title" is longer than the screen takes is left out; one such row turned the whole list down as well. - Said in the log: how many songs were left unlisted, and, should the list still be turned down, what it held. Logs - Spotify device: librespot's own warnings and errors are written at the warning level again; only the rest of what it says stays at debug. A player whose Spotify connects and stays silent had nothing to send. - Multiplayer: a message turned down with "Join code, sender identity, or multiplayer protocol did not match" now says which of the three it was, its size and first bytes, and whether it came from a dedicated server. --- .../Game/Build/20260929/shader_permutations.h | 19 +++++ .../Multiplayer/Session/session_receive.cpp | 16 +++++ Extension/Music/local_music_assets.cpp | 35 +++++++-- Extension/Music/local_music_ui.cpp | 23 ++++-- Extension/Music/local_music_ui.h | 6 ++ Extension/Music/spotify_device.cpp | 5 +- Extension/Music/spotify_rows.cpp | 5 ++ Extension/Music/spotify_rows.h | 3 + Extension/World/shader_permutation_guard.cpp | 72 +++++++++++++++++++ Extension/World/shader_permutation_guard.h | 16 +++++ Runtime/runtime_init.cpp | 2 + cmake/Runtime.cmake | 1 + 12 files changed, 191 insertions(+), 12 deletions(-) create mode 100644 Engine/Game/Build/20260929/shader_permutations.h create mode 100644 Extension/World/shader_permutation_guard.cpp create mode 100644 Extension/World/shader_permutation_guard.h diff --git a/Engine/Game/Build/20260929/shader_permutations.h b/Engine/Game/Build/20260929/shader_permutations.h new file mode 100644 index 0000000..7dc8cf5 --- /dev/null +++ b/Engine/Game/Build/20260929/shader_permutations.h @@ -0,0 +1,19 @@ +#pragma once +#include "Engine/Game/Build/fingerprint.h" +#include +#include + +// The game's expression shaders (Extension/World/shader_permutation_guard.cpp). +namespace dingosdk::game::build::v20260929::shader_permutations { +// prepare(shader, renderer, state, bool, bool): run on a job worker (from 0x3f72b90, for every +// shader each view has registered, and from 0x3f79b70 for one). The shader keeps its compiled +// permutations as an array of pointers (begin at +0x50, end at +0x58). The function asks the +// first one for its kind (its first virtual function: ExpressionShaderRasterPermutation or +// ExpressionShaderRaytracePermutation) without checking it is there, then hands each to +// 0x46aa370 or 0x46aac20: +// mov rax,[rcx+0x50]; ...; mov rcx,[rax]; mov rax,[rcx] <- 0x3f86da6, rcx null +inline constexpr Fingerprint prepare{0x3f86d80, { + 0x48,0x89,0x5c,0x24,0x18,0x48,0x89,0x6c,0x24,0x20,0x56,0x57,0x41,0x54,0x48,0x83, + 0xec,0x40,0x48,0x8b,0x41,0x50,0x48,0x8b,0xf1,0x45,0x0f,0xb6,0xe1,0x49,0x8b,0xd8}}; +inline constexpr std::uintptr_t permutations_begin = 0x50, permutations_end = 0x58; +} diff --git a/Extension/Multiplayer/Session/session_receive.cpp b/Extension/Multiplayer/Session/session_receive.cpp index ecb82eb..47cbd47 100644 --- a/Extension/Multiplayer/Session/session_receive.cpp +++ b/Extension/Multiplayer/Session/session_receive.cpp @@ -1,8 +1,10 @@ #include "session_internal.h" #include "Extension/Profile/local_profile_runtime.h" #include "Extension/Multiplayer/Net/wire_codec.h" +#include "Engine/Core/Log/logging.h" #include "Engine/Game/Build/supported_build.h" #include +#include #include #include #include @@ -593,6 +595,20 @@ void networking(Session &s, const NativeFrame &local, std::uint64_t now) { continue; if (!decoded || decoded->session != s.secret || ((s.mode == Mode::host || direct_link) && decoded->source != message.peer)) { + // Which of the three it was, and what the message looked like: the status line + // says only that one of them failed (2026-10-13: joins to updated servers ended + // here with nothing to tell them apart). + try { + std::string start; + for (std::size_t i = 0; i < message.bytes.size() && i < 16; ++i) start += std::format("{:02x}", static_cast(message.bytes[i])); + dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::runtime, + "Multiplayer: a message from {} was turned down: {}. {} byte(s), starting {}; {} link{}{}.", message.peer, + !decoded ? std::string("it could not be read") + : decoded->session != s.secret ? std::format("it is of another session (kind {})", static_cast(decoded->kind)) + : std::format("it names another sender, {} (kind {})", decoded->source, static_cast(decoded->kind)), + message.bytes.size(), start, direct_link ? "a direct" : "the host's", batch.empty() ? "" : ", one of a batch", + s.mode == Mode::join && dedicated_host(s) ? ", a dedicated server" : ""); + } catch (...) {} disconnect(s, message.peer, "Join code, sender identity, or multiplayer protocol did not match."); if (s.mode == Mode::off) return; diff --git a/Extension/Music/local_music_assets.cpp b/Extension/Music/local_music_assets.cpp index cc1899c..fee3e79 100644 --- a/Extension/Music/local_music_assets.cpp +++ b/Extension/Music/local_music_assets.cpp @@ -98,16 +98,41 @@ bool read_music_catalog(MusicCatalog& result) { // For each station, its rows' ids and the Spotify track each is (none for a row that only // says what picking the playlist does). std::vector>> spotify_row_ids; + // The music screen takes only so many songs, and so many places in playlists, and with more + // is given none at all (music_ui_catalog_valid). Rows are registered while there is room, + // under those by a margin for the game's own; the rest of a playlist is left out of its rows + // (it still plays through: only its list is shorter). + std::uint32_t registered{}; + read(manager + 0x84, registered); + std::size_t room = registered + 200 < music_ui_most_songs ? music_ui_most_songs - 200 - registered : 0; + std::size_t places = music_ui_most_places / 2, left_out{}; for (const auto& theirs : spotify_playlists) { auto& rows = spotify_row_ids.emplace_back(); - for (const auto& track : theirs.tracks) - if (music_ui_text(track.artist, 200) && music_ui_text(track.title, 200)) - if (auto id = spotify_rows::ensure(base, manager, track.artist, track.title); !id.empty() && - std::none_of(rows.begin(), rows.end(), [&](const auto& row) { return row.first == id; })) - rows.emplace_back(std::move(id), track.uri); + for (const auto& track : theirs.tracks) { + // (Its id, "artist - title", has to be one the screen takes as well.) + if (!music_ui_text(track.artist, 200) || !music_ui_text(track.title, 200) || track.artist.size() + track.title.size() + 3 > 255) continue; + const bool known = spotify_rows::known(track.artist, track.title); + if (!places || (!known && !room)) { + ++left_out; + continue; + } + if (auto id = spotify_rows::ensure(base, manager, track.artist, track.title); !id.empty() && + std::none_of(rows.begin(), rows.end(), [&](const auto& row) { return row.first == id; })) { + rows.emplace_back(std::move(id), track.uri); + --places; + if (!known) --room; + } + } if (theirs.id == spotify_station::plain_station) rows.emplace_back(spotify_rows::ensure(base, manager, "Spotify", "Start Radio"), std::string()); else if (rows.empty() && music_ui_text(theirs.name, 200)) rows.emplace_back(theirs.id == external_audio::station ? spotify_rows::ensure(base, manager, external_audio::station_name, "Start") : spotify_rows::ensure(base, manager, "Spotify", "Play " + theirs.name), std::string()); } + if (static std::size_t said{}; left_out != said) { + said = left_out; + if (left_out) + dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::music, + "Music screen: {} song(s) of your playlists are not listed: it has room for about {} songs in all. They still play; fewer or shorter playlists list them all.", + left_out, music_ui_most_songs); + } native.lock(manager + 0x20); struct Unlock { game::NativeModelFunctions::Lock function; diff --git a/Extension/Music/local_music_ui.cpp b/Extension/Music/local_music_ui.cpp index 870eb21..b120df0 100644 --- a/Extension/Music/local_music_ui.cpp +++ b/Extension/Music/local_music_ui.cpp @@ -31,9 +31,9 @@ std::string music_artwork_url(std::string_view value) { // MusicGraphAsset metadata/TagRefs. No generated catalog, guessed memberships, // artwork or audio-residency gate. Favorite identities are saved separately in -// the local profile and applied after song publication. Bounds: 1024 songs, +// the local profile and applied after song publication. Bounds: 2000 songs, -// 256 authored groups, 8192 membership edges. Include after assets/news/model helpers. +// 256 authored groups, 16384 membership edges (music_ui_most_songs, music_ui_most_places). Include after assets/news/model helpers. void music_release_weak(std::uintptr_t owner) noexcept { if (owner && InterlockedDecrement(reinterpret_cast(owner + 12)) == 0) { @@ -117,19 +117,19 @@ bool music_ui_text(std::string_view value, std::size_t limit ) { } bool music_ui_catalog_valid(const MusicCatalog& catalog, const std::string& favorites) { - if (catalog.songs.empty() || catalog.songs.size() > 1024 || catalog.playlists.empty() || catalog.playlists.size() > 256) return false; + if (catalog.songs.empty() || catalog.songs.size() > music_ui_most_songs || catalog.playlists.empty() || catalog.playlists.size() > 256) return false; std::set songs, playlists; std::set> declared, grouped; for (const auto& song : catalog.songs) { if (!music_ui_text(song.id) || !music_ui_text(song.artist, 512) || !music_ui_text(song.title, 512) || song.id != song.artist + " - " + song.title || !songs.insert(song.id).second) return false; for (const auto& playlist : song.playlists) - if (!music_ui_text(playlist) || !declared.emplace(playlist, song.id).second || declared.size() > 8192) return false; + if (!music_ui_text(playlist) || !declared.emplace(playlist, song.id).second || declared.size() > music_ui_most_places) return false; } for (const auto& playlist : catalog.playlists) { if (!music_ui_text(playlist.id) || playlist.id == favorites || !playlists.insert(playlist.id).second) return false; for (const auto& id : playlist.songs) - if (!songs.contains(id) || !grouped.emplace(playlist.id, id).second || grouped.size() > 8192) return false; + if (!songs.contains(id) || !grouped.emplace(playlist.id, id).second || grouped.size() > music_ui_most_places) return false; } return declared == grouped; } @@ -333,7 +333,18 @@ void update_music_catalog() { if (!music_ui_current(*pending)) { retry = false; return; } std::string favorite_id; if (!identifier(reinterpret_cast(pending->manager + 8), favorite_id) || - !music_ui_catalog_valid(catalog, favorite_id)) throw std::runtime_error("Invalid runtime music catalog"); + !music_ui_catalog_valid(catalog, favorite_id)) { + // (Said once, with what it held: an empty music screen has this to go on.) + static bool said{}; + if (!std::exchange(said, true)) { + std::size_t places{}; + for (const auto& playlist : catalog.playlists) places += playlist.songs.size(); + dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::music, + "Music screen: its list of songs was turned down, and it is empty: {} song(s) (the most is {}), {} playlist(s) (256), {} place(s) in playlists ({}).", + catalog.songs.size(), music_ui_most_songs, catalog.playlists.size(), places, music_ui_most_places); + } + throw std::runtime_error("Invalid runtime music catalog"); + } std::uintptr_t arena{}, vtable{}; if (!read(s.base + addr::engine::default_arena, arena) || !arena || !read(arena, vtable) || vtable < s.base || vtable >= s.base + supported_build::game_image_size) return; ui.functions.allocator[1] = arena; diff --git a/Extension/Music/local_music_ui.h b/Extension/Music/local_music_ui.h index 92bb93f..886bf79 100644 --- a/Extension/Music/local_music_ui.h +++ b/Extension/Music/local_music_ui.h @@ -62,6 +62,12 @@ bool music_ui_current(const MusicUiPending& pending); CosmeticShared music_ui_lease(const MusicUiPending& pending); bool music_ui_text(std::string_view value, std::size_t limit = 255); +// The most songs the music screen is given, and the most places songs have in playlists: over +// either, music_ui_catalog_valid turns the whole list down and the screen is empty. The rows of +// the player's own playlists (Spotify, YouTube Music, their files) are held under them +// (local_music_assets.cpp): a few large playlists went over the old thousand (2026-10-13). +// (Two thousand songs: the screen's own table of songs is read only up to 2048, music_ui_context.) +inline constexpr std::size_t music_ui_most_songs = 2000, music_ui_most_places = 16384; bool music_ui_catalog_valid(const MusicCatalog& catalog, const std::string& favorites); diff --git a/Extension/Music/spotify_device.cpp b/Extension/Music/spotify_device.cpp index 6d0bc59..39d48ab 100644 --- a/Extension/Music/spotify_device.cpp +++ b/Extension/Music/spotify_device.cpp @@ -79,7 +79,10 @@ void told(const char* text) { if (parts.empty()) return; if (parts[0] == "LOG") { static std::atomic logged{}; - if (parts.size() > 1 && logged.fetch_add(1, std::memory_order_relaxed) < 300) say(logging::Level::debug, "(librespot) " + parts[1]); + // (What it says went wrong is for everyone's log: a player whose Spotify stays silent + // has it to send. The rest is for working on it.) + const bool wrong = parts.size() > 1 && (parts[1].starts_with("ERROR") || parts[1].starts_with("WARN")); + if (parts.size() > 1 && logged.fetch_add(1, std::memory_order_relaxed) < 300) say(wrong ? logging::Level::warning : logging::Level::debug, "(librespot) " + parts[1]); // (What it says just before it gives up on an account that is not Premium.) if (parts.size() > 1 && parts[1].find("does not support") != std::string::npos && parts[1].find("accounts") != std::string::npos) not_premium.store(true); } else if (parts[0] == "READY") { diff --git a/Extension/Music/spotify_rows.cpp b/Extension/Music/spotify_rows.cpp index 623a350..e177241 100644 --- a/Extension/Music/spotify_rows.cpp +++ b/Extension/Music/spotify_rows.cpp @@ -248,6 +248,11 @@ bool install(std::uintptr_t base) noexcept { return true; } +bool known(const std::string& artist, const std::string& title) { + std::lock_guard lock(mutex); + return rows.contains(artist + " - " + title); +} + std::string ensure(std::uintptr_t base, std::uintptr_t manager, const std::string& artist, const std::string& title) { if (!base || !manager || artist.empty() || title.empty()) return {}; if (!matches(base, lm::song_register_contract) || !matches(base, lm::song_lookup_contract)) { diff --git a/Extension/Music/spotify_rows.h b/Extension/Music/spotify_rows.h index f7ba44c..3b9a8ce 100644 --- a/Extension/Music/spotify_rows.h +++ b/Extension/Music/spotify_rows.h @@ -12,6 +12,9 @@ namespace dingosdk::spotify_rows { // yet: its id in the music screen ("artist - title"), or empty when it could not be made. // The thread that reads the catalog; not with the manager's lock held. std::string ensure(std::uintptr_t base, std::uintptr_t manager, const std::string& artist, const std::string& title); +// Whether a row of that artist and title has been registered already (it then costs the music +// screen nothing more to use it again). +bool known(const std::string& artist, const std::string& title); // The id ("artist - title") of the row known to the game by these 16 bytes, or empty. std::string id_of(const unsigned char* row); // Watches for the game beginning to play one of these (the player picked its row), which diff --git a/Extension/World/shader_permutation_guard.cpp b/Extension/World/shader_permutation_guard.cpp new file mode 100644 index 0000000..3c3c93a --- /dev/null +++ b/Extension/World/shader_permutation_guard.cpp @@ -0,0 +1,72 @@ +#include "shader_permutation_guard.h" +#include "Engine/Core/Hooks/hooks.h" +#include "Engine/Core/Log/logging.h" +#include "Engine/Core/Platform/memory.h" +#include "Engine/Game/Build/addresses.h" +#include "Engine/Game/Build/20260929/shader_permutations.h" +#include +#include + +namespace dingosdk::shader_permutation_guard { +namespace { +namespace sp = addr::shader_permutations; +using Prepare = void (*)(std::uintptr_t, void*, void*, std::uint8_t, std::uint8_t); +Prepare original{}; +std::atomic installed{}; +std::atomic skips{}; + +// Whether every permutation the game is about to use is there. The game reads the first one +// whatever the array's length, so an array without storage counts as missing too. This is +// asked for every shader of every view: two reads and a short walk, nothing more. +bool complete(std::uintptr_t shader) noexcept { + if (!shader) return true; // (not this guard's: the game's own fault, left as it is) + const auto *const begin = *reinterpret_cast(shader + sp::permutations_begin); + const auto *const end = *reinterpret_cast(shader + sp::permutations_end); + if (!begin || !*begin) return false; + if (end < begin || end - begin > 4096) return true; + for (const auto *at = begin; at != end; ++at) + if (!*at) return false; + return true; +} +// Said for the first few, then every thousandth: a shader that is passed over is passed over +// on every pass. +__declspec(noinline) void note_skip(std::uintptr_t shader) noexcept { + const auto count = skips.fetch_add(1, std::memory_order_relaxed) + 1; + if (count > 8 && count % 1000) return; + try { + logging::log(logging::Level::warning, logging::Channel::runtime, + "Shaders: a shader ({:#x}) has a compiled permutation missing, where the game would have crashed; it is not " + "prepared, and what uses it may not be drawn ({} time(s) since the game started). A mod's material that names a " + "shader the game could not build does this.", shader, count); + } catch (...) {} +} +void guarded(std::uintptr_t shader, void* renderer, void* state, std::uint8_t fourth, std::uint8_t fifth) { + if (!complete(shader)) { + note_skip(shader); + return; + } + original(shader, renderer, state, fourth, fifth); +} +} // namespace + +bool install(std::uintptr_t base) noexcept { + if (installed.exchange(true)) return original != nullptr; + if (!base) return false; + std::array head{}; + if (!memory::peek(base + sp::prepare.rva, head) || head != sp::prepare.bytes) { + logging::log(logging::Level::warning, logging::Channel::runtime, + "Shader permutation guard: this build's shader code differs; it is left as it is."); + return false; + } + const auto target = reinterpret_cast(base + sp::prepare.rva); + if (hook_prepare(target, reinterpret_cast(&guarded), reinterpret_cast(&original)) != HookOk || + hook_enable(target) != HookOk) { + original = nullptr; + logging::log(logging::Level::warning, logging::Channel::runtime, "Shader permutation guard: the game's function could not be hooked."); + return false; + } + return true; +} + +std::uint64_t skipped() noexcept { return skips.load(std::memory_order_relaxed); } +} // namespace dingosdk::shader_permutation_guard diff --git a/Extension/World/shader_permutation_guard.h b/Extension/World/shader_permutation_guard.h new file mode 100644 index 0000000..5ed62a4 --- /dev/null +++ b/Extension/World/shader_permutation_guard.h @@ -0,0 +1,16 @@ +#pragma once +#include + +// The most reported crash of 2.0.8 (Sentry, 2026-10-13: Skate.exe+0x3f86da6 on some thirty +// machines within the hour): as the game prepares its shaders on a job worker, it reads the +// first compiled permutation of each without checking there is one +// (Engine/Game/Build//shader_permutations.h). A shader whose permutation never came to +// be (a material that names a shader program the game could not build) holds null there. The +// guard passes such a shader over: it is not prepared, and what it would have drawn may be +// missing, where the game would have closed. +namespace dingosdk::shader_permutation_guard { +// Checks this build's function and hooks it; once. False when the build differs (no guard). +bool install(std::uintptr_t base) noexcept; +// How many times a shader was passed over since the game started. +std::uint64_t skipped() noexcept; +} diff --git a/Runtime/runtime_init.cpp b/Runtime/runtime_init.cpp index e22d1d1..383ed04 100644 --- a/Runtime/runtime_init.cpp +++ b/Runtime/runtime_init.cpp @@ -8,6 +8,7 @@ #include "Extension/Settings/replay_guard.h" #include "Extension/World/load_error_guard.h" #include "Extension/World/mesh_unload_guard.h" +#include "Extension/World/shader_permutation_guard.h" #include "Extension/World/stack_check_report.h" #include "Extension/Music/music_inject.h" #include "Extension/Music/spotify_web.h" @@ -327,6 +328,7 @@ extern "C" __declspec(dllexport) BOOL WINAPI DingoSDKDebugInitialize() { (void)dingosdk::engine_tweaks::install(r.base); (void)dingosdk::replay_guard::install(r.base); (void)dingosdk::mesh_unload_guard::install(r.base); + (void)dingosdk::shader_permutation_guard::install(r.base); (void)dingosdk::load_error_guard::install(r.base); (void)dingosdk::texture_handle_limit::install(r.base); (void)dingosdk::stack_check_report::install(r.base); diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index eb23e16..1ce5edf 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -84,6 +84,7 @@ add_library(dingosdk_runtime SHARED Extension/Music/app_audio_capture.cpp Extension/Music/music_inject.cpp Extension/World/mesh_unload_guard.cpp + Extension/World/shader_permutation_guard.cpp Extension/World/stack_check_report.cpp Extension/World/texture_handle_limit.cpp Extension/Settings/replay_drafts.cpp