Files
AyeZeeBB 719cba1166 Freezes and crashes: the game's threads no longer wait on ReSkate's slow work
Players reported the game hanging for about a second while skating. A
profile showed the memory line in the log walking every heap
(HeapSummary) on the client thread, 0.5 to 2 s every two minutes. An
audit of everything that runs on the game's update and render threads
found more of the same, and Sentry's top crashes had causes of ours.

Freezes:
- Memory log line: no heap walk or thread snapshot; the GPU part and the
  write happen on a thread of their own.
- Profile database: synchronous=NORMAL in WAL mode. A save is still kept
  when the game crashes; every play event no longer waits for the disk
  inside the game's hooks.
- ReSkate's own preferences and values (menu clicks, session sliders,
  Discord) are saved by a background writer in order; reads see a value
  until it is saved.
- Input: the foreground window and the keyboard are sampled on a thread
  of their own (GetAncestor and GetAsyncKeyState stalled the client tick
  40-50 ms when another program held Windows' lock); flight input uses
  the sample; ClipCursor at most twice a second while a menu has the
  pointer.
- Hooks installed while the game runs go in one transaction per place
  (HookBatch): throwdowns, remote players, the party, Hall of Meat.
- Off the update thread: the host's map package lookup (one table read in
  the background, not the Mods folder per offer), the trainer's map file
  and settings save, Board Break's settings save, mod music playlists and
  their covers, the Spotify station covers.
- Off the render thread: the Modding page's and the deck card's Mods
  folder reads. The overlay's three startup waits share five seconds.
- Less work per frame: Steam names reused while one a frame is asked
  again, 2 friends a tick, steam_api64.dll checked once per process,
  cosmetic names read in place rather than a system call per character,
  an outfit load no longer copies the inventory per slot, the throwdown
  type scan runs at background priority over resident memory, the Board
  Break deck label is kept a minute.

Crashes:
- Render-resource pool: the metadata fields are written to the state
  owner only. The other renderer is 0x2daa00 bytes, and +0x11aae28 is far
  past its end (store_field crash; heap corruption in every release).
- Other players' skateboard effects: a stale skateboard component is no
  longer followed, and the joint lookup is walked with a bound first (an
  endless loop in reused memory ended in GameStall).
- Launcher: Oodle decodes into the buffer it asks for with corrupt data
  possible, and its assertions no longer stop the process (heap corruption
  in the store-copy scan; a breakpoint in oo2core).

Timings (Send performance timings):
- A frame of half a second or more in a level is sent as game.freeze:
  where the time went (ReSkate's tick, the game's frame, or outside it),
  whether the renderer kept presenting, and ReSkate's busiest zones.
- Closing the game sends the level session it was in.

Music after a connection drop:
- Spotify device: an account Spotify could not be reached with is kept and
  tried again (3 s doubling to a minute); a session that drops is taken
  up again. Only an account Spotify refuses is asked for anew.
- YouTube playlist pictures that could not be fetched are tried again,
  from 30 s to 10 minutes apart.
2026-10-15 18:10:53 -05:00

183 lines
8.8 KiB
C++

#include "steam_social.h"
#include "steam_friend_join.h"
#include "Engine/Core/Platform/launcher_support.h"
#include <Windows.h>
#include <algorithm>
#include <filesystem>
#include <mutex>
#include <stdexcept>
namespace dingosdk::multiplayer {
namespace {
template<class T> T symbol(HMODULE module, const char *name) {
const auto address = GetProcAddress(module, name);
if (!address) throw std::runtime_error("Steam social export unavailable");
#pragma warning(push)
#pragma warning(disable : 4191)
return reinterpret_cast<T>(address);
#pragma warning(pop)
}
std::string name(const char *value) {
if (!value) return {};
std::string result;
for (unsigned i = 0; i < 128 && value[i]; ++i)
if (static_cast<unsigned char>(value[i]) >= 32 && value[i] != 127) result += value[i];
return result;
}
struct Api {
HMODULE module{};
int (*user_handle)(){};
void *(*friends)(){};
void *(*user)(){};
std::uint64_t (*id)(void *){};
bool (*logged_on)(void *){};
const char *(*self_name)(void *){};
int (*count)(void *, int){};
std::uint64_t (*at)(void *, int, int){};
const char *(*friend_name)(void *, std::uint64_t){};
int (*presence)(void *, std::uint64_t){};
struct Game { std::uint64_t id{}; std::uint32_t ip{}; std::uint16_t port{}, query{}; std::uint64_t lobby{}; };
bool (*game)(void *, std::uint64_t, Game *){};
const char *(*rich)(void *, std::uint64_t, const char *){};
void load() {
const auto loaded = GetModuleHandleW(L"steam_api64.dll");
if (!loaded || loaded == module) return;
std::wstring path(32768, L'\0');
const auto length = GetModuleFileNameW(loaded, path.data(), static_cast<DWORD>(path.size()));
if (!length || length >= path.size()) return;
path.resize(length); launcher::validate_steam_api_file(std::filesystem::path(path));
user_handle = symbol<decltype(user_handle)>(loaded, "SteamAPI_GetHSteamUser");
friends = symbol<decltype(friends)>(loaded, "SteamAPI_SteamFriends_v017");
user = symbol<decltype(user)>(loaded, "SteamAPI_SteamUser_v023");
id = symbol<decltype(id)>(loaded, "SteamAPI_ISteamUser_GetSteamID");
logged_on = symbol<decltype(logged_on)>(loaded, "SteamAPI_ISteamUser_BLoggedOn");
#define BIND(member, suffix) member = symbol<decltype(member)>(loaded, "SteamAPI_ISteamFriends_" suffix)
BIND(self_name, "GetPersonaName"); BIND(count, "GetFriendCount"); BIND(at, "GetFriendByIndex");
BIND(friend_name, "GetFriendPersonaName"); BIND(presence, "GetFriendPersonaState");
BIND(game, "GetFriendGamePlayed"); BIND(rich, "GetFriendRichPresence");
#undef BIND
module = loaded;
}
};
}
std::shared_ptr<const SteamSocialSnapshot> steam_social_snapshot() {
static Api api;
static std::mutex mutex;
static auto current = std::make_shared<const SteamSocialSnapshot>();
static ULONGLONG next{};
// Each friend costs four calls into the Steam client, and the game thread asks for
// this snapshot every frame. Read the list a few friends per clock tick (~16 ms)
// and publish it once complete, so no caller pays for the whole list at once.
static struct Refresh {
bool active{};
int index{}, count{};
ULONGLONG tick{};
void *friends{};
SteamSocialSnapshot result;
} refresh;
// (Two: each is four or five calls into the Steam client, on the client thread, and a call waits
// whenever Steam is busy. A pass over a few hundred friends still takes a few seconds.)
constexpr int friends_per_tick = 2;
std::lock_guard lock(mutex);
const auto now = GetTickCount64();
const auto publish = [&](SteamSocialSnapshot &result) {
if (result.local != current->local || result.friends != current->friends) {
result.revision = current->revision + 1;
current = std::make_shared<const SteamSocialSnapshot>(std::move(result));
}
};
if (!refresh.active) {
if (now < next) return current;
next = now + 10000;
SteamSocialSnapshot result;
if (launcher::offline_mode()) {
result.local = {launcher::offline_steam_id(), launcher::offline_player_name, false, true};
if (result.local != current->local) {
result.revision = current->revision + 1;
current = std::make_shared<const SteamSocialSnapshot>(std::move(result));
}
return current;
}
try {
api.load();
if (api.module && api.user_handle()) {
const auto user = api.user(), friends = api.friends();
if (user && friends) {
result.local = {api.id(user), name(api.self_name(friends)), api.logged_on(user), true};
// The local player is known now: publish it at once (with the friends
// already known) rather than after the whole list has been read.
if (result.local != current->local) {
SteamSocialSnapshot local = *current;
local.local = result.local;
local.revision = current->revision + 1;
current = std::make_shared<const SteamSocialSnapshot>(std::move(local));
}
// Immediate friends only; exclude blocked/requested/ignored accounts.
refresh.count = std::clamp(api.count(friends, 4), 0, 2048);
refresh.index = 0;
refresh.tick = 0;
refresh.friends = friends;
refresh.result = std::move(result);
refresh.result.friends.reserve(static_cast<std::size_t>(refresh.count));
refresh.active = true;
}
}
} catch (...) { return current; }
if (!refresh.active) {
publish(result);
return current;
}
}
if (now == refresh.tick) return current;
refresh.tick = now;
try {
if (!api.user_handle()) {
refresh = {};
return current;
}
auto &result = refresh.result;
for (const auto end = std::min(refresh.count, refresh.index + friends_per_tick); refresh.index < end; ++refresh.index) {
const auto id = api.at(refresh.friends, refresh.index, 4);
if (!id || id == result.local.id) continue;
const auto presence = api.presence(refresh.friends, id);
Api::Game game;
const bool playing = api.game(refresh.friends, id, &game) && (game.id & 0xffffff) == 3354750;
// Where they skate: what their game says for this, or (a build before it did) the
// joinable session it offers friends.
std::uint64_t session{};
if (playing) {
session = steam_session_target(name(api.rich(refresh.friends, id, steam_session_key.data()))).value_or(0);
if (!session) session = steam_join_target(name(api.rich(refresh.friends, id, "connect"))).value_or(0);
}
result.friends.push_back({id, name(api.friend_name(refresh.friends, id)), presence > 0 && presence < 7, playing, session});
}
if (refresh.index < refresh.count) return current;
// The list changed during the pass, so an index may have shifted past a friend:
// keep the ones read last time that this pass missed (a friend really removed
// goes at the next pass).
if (std::clamp(api.count(refresh.friends, 4), 0, 2048) != refresh.count)
for (const auto &known : current->friends)
if (std::none_of(result.friends.begin(), result.friends.end(),
[&](const auto &read) { return read.id == known.id; }))
result.friends.push_back(known);
// The list can change between ticks, shifting an index: keep each friend once.
std::sort(result.friends.begin(), result.friends.end(), [](const auto &a, const auto &b) { return a.id < b.id; });
result.friends.erase(std::unique(result.friends.begin(), result.friends.end(),
[](const auto &a, const auto &b) { return a.id == b.id; }),
result.friends.end());
std::sort(result.friends.begin(), result.friends.end(), [](const auto &a, const auto &b) {
if (a.playing != b.playing) return a.playing;
if (a.online != b.online) return a.online;
return a.id < b.id;
});
publish(result);
} catch (...) {}
refresh = {};
// A pass reads every friend (four Steam calls each) on the game thread: profiled at 0.4% of
// the client update with a full list read every 2 s (2026-10-01). Every 10 s is plenty for
// the friends list; the local player's own entry still refreshes at the start of each pass.
next = now + 10000;
return current;
}
}