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

236 lines
10 KiB
C++

#include "hall_of_meat.h"
#include "Engine/Core/Hooks/hooks.h"
#include "hall_of_meat_hud.h"
#include "hall_of_meat_overlay.h"
#include "hall_of_meat_render.h"
#include "hall_of_meat_skater.h"
#include "hall_of_meat_skeleton.h"
#include "hall_of_meat_slow_motion.h"
#include "Engine/Core/Log/logging.h"
#include "Extension/Profile/local_profile_runtime.h"
#include "Extension/Skater/no_bail.h"
#include <Windows.h>
#include <algorithm>
#include <atomic>
#include <cmath>
#include <format>
#include <string>
#include <vector>
namespace dingosdk::hall_of_meat {
namespace {
constexpr const char* preference = "HallOfMeat";
constexpr std::string_view best_prefix = "HallOfMeat.Best."; // + the level, lower case
struct State {
std::atomic<bool> ready{}, on{}; // the switch: started, and switched on
SRWLOCK lock = SRWLOCK_INIT; // guards the rest; never held across game reads
Tracker tracker;
bool summary_pending{};
Summary summary;
// The current map's best Meat (known once the client tick named the map), where the last
// finished bail that showed stands against it, and whether a new best is still to be saved.
bool best_known{}, best_unsaved{};
int best{};
Standing standing;
std::string level; // client thread only
SlowMotion slow_motion; // client thread only: after a break
bool active{}; // client thread only: the hooks on, as the switch is or until what it changed is given back
};
State& state() { static auto* value = new State; return *value; }
// A bail is over (under the lock): it is logged on the client tick, and stands against the map's best.
void finished(State& s, const Summary& ended) noexcept {
s.summary = ended;
s.summary_pending = true;
s.standing = ended.shown && s.best_known ? standing(s.best, ended.tally.score) : Standing{};
if (s.standing.new_best) {
s.best = s.standing.best;
s.best_unsaved = true;
}
}
// Physics thread, every step of the local skater (no_bail.h): what each body touched in the step
// and whether it is a ragdoll, and so where the bail is and what it did.
void observe_step(const NoBailSkater& skater, float seconds, bool wipeout) noexcept {
if (!enabled()) return;
auto& s = state();
const auto now = GetTickCount64();
const auto step = read_step(skater, seconds, wipeout);
Summary ended;
AcquireSRWLockExclusive(&s.lock);
if (s.tracker.step(now, step, &ended)) finished(s, ended);
ReleaseSRWLockExclusive(&s.lock);
}
// One debug line per bail: what it scored and why.
void log_bail(const Summary& summary) {
std::vector<std::size_t> order;
for (std::size_t index = 1; index < skater_body::count; ++index)
if (summary.peaks[index] > 0) order.push_back(index);
std::sort(order.begin(), order.end(), [&](auto a, auto b) { return summary.peaks[a] > summary.peaks[b]; });
std::string hits;
for (const auto index : order) {
const float peak = summary.peaks[index];
if (peak < hit_speed && !hits.empty()) break;
hits += std::format("{}{} {:.1f} m/s{}", hits.empty() ? "" : ", ", skater_body::names[index], peak,
peak >= broken_speed ? " (broken)" : peak >= hit_speed ? " (hit)" : " (hardest, not hurt)");
}
std::string scrapes;
for (std::size_t index = 1; index < skater_body::count; ++index)
if (summary.scraped[index] >= 0.05f)
scrapes += std::format("{}{} {:.1f} m", scrapes.empty() ? "" : ", ", skater_body::names[index], summary.scraped[index]);
const auto& tally = summary.tally;
logging::log(logging::Level::debug, logging::Channel::skater,
"Hall of Meat: bail over, {} Meat ({} damage: {} from {} impacts, head +{}, vehicle +{}, road rash {:.1f} m +{}; "
"{} broken +{}; {:.1f} s +{}; {:.1f} s airtime +{}; fell {:.1f} m +{}; top speed {:.1f} m/s +{}; {:.1f} rotations +{}). "
"Hits: {}. Road rash: {}",
tally.score, tally.damage, tally.hit_points, tally.impacts, tally.head_bonus, tally.vehicle_bonus,
static_cast<double>(tally.scraped), tally.scrape_points, tally.broken, tally.broken * points_per_break,
static_cast<double>(tally.seconds), tally.time_points, static_cast<double>(tally.airtime), tally.airtime_points,
static_cast<double>(tally.fallen), tally.fall_points, static_cast<double>(tally.top_speed), tally.speed_points,
static_cast<double>(tally.rotations), tally.rotation_points,
hits.empty() ? std::string("no body contact") : hits, scrapes.empty() ? std::string("none") : scrapes);
}
std::string best_key(std::string_view level) {
std::string key(best_prefix);
for (const char c : level) key += c >= 'A' && c <= 'Z' ? static_cast<char>(c - 'A' + 'a') : c;
return key;
}
int saved_best(std::string_view level) noexcept {
const auto value = profile_runtime::local_value(best_key(level));
if (!value || !value->is_number()) return 0;
const auto best = value->get<double>();
return std::isfinite(best) && best > 0 && best < 1e9 ? static_cast<int>(best) : 0;
}
}
bool start(std::uintptr_t base) noexcept {
auto& s = state();
if (s.ready.load(std::memory_order_acquire)) return true;
if (!start_skater(base)) return false; // logged
// The hooks are prepared here and switched on and off with the feature (on_client_tick).
(void)start_render(base); // without it no skeleton shows: logged
start_hud(base);
const bool on = profile_runtime::local_preference(preference).value_or(false);
// The overlay builds its atlas once, when its graphics start, so the images are read now.
overlay::prepare_hall_of_meat_images();
s.on.store(on, std::memory_order_release);
if (on) set_no_bail_step_observer(&observe_step);
s.ready.store(true, std::memory_order_release);
logging::log(logging::Level::info, logging::Channel::skater, "Hall of Meat ready ({}).", on ? "on" : "off");
return true;
}
void on_client_tick() noexcept {
auto& s = state();
if (!s.ready.load(std::memory_order_acquire)) return;
const bool on = enabled();
if (!on && !s.active) return; // switched off: nothing hooked, nothing to give back
if (on && !s.active) {
prepare_skeleton(); // once, in the background
{
HookBatch batch; // (one pause of the game's threads for both)
(void)hook_render(true);
(void)hook_hud(true);
}
s.active = true;
}
try {
NoBailSkater skater;
const bool gone = !no_bail_skater(skater);
const auto now = GetTickCount64();
Summary summary, ended;
bool pending{}, unsaved{}, card{};
int best{};
float speed{};
AcquireSRWLockExclusive(&s.lock);
if (gone && s.tracker.lose(now, &ended)) finished(s, ended);
card = s.tracker.view(now).phase != Phase::riding;
speed = s.tracker.game_speed(now);
std::swap(pending, s.summary_pending);
if (pending) summary = s.summary;
std::swap(unsaved, s.best_unsaved);
best = s.best;
ReleaseSRWLockExclusive(&s.lock);
// The renderer's picture of the skater is only read while a bail shows.
follow_render(card);
// The card takes the place of skate.'s bottom left HUD while it shows: the bail is what counts.
const bool corner_back = hide_hud(card);
// A break slows the game down (in single player only: hall_of_meat_slow_motion.h).
(void)s.slow_motion.set(speed);
if (pending && logging::enabled(logging::Level::debug)) log_bail(summary);
if (unsaved && !s.level.empty()) profile_runtime::set_local_values({{best_key(s.level), static_cast<double>(best)}});
// Switched off (set_enabled ended the bail): the hooks go once the corner and the game's speed are given back.
if (!on && corner_back && s.slow_motion.idle() && hook_hud(false) && hook_render(false)) s.active = false;
} catch (...) { /* A lost log line or best is never worth the client tick. */ }
}
void set_level(std::string_view level) noexcept {
auto& s = state();
if (!s.ready.load(std::memory_order_acquire) || level == s.level) return;
try {
s.level = level;
const int best = level.empty() ? 0 : saved_best(level);
AcquireSRWLockExclusive(&s.lock);
s.best_known = !level.empty();
s.best = best;
s.best_unsaved = false;
s.standing = {};
ReleaseSRWLockExclusive(&s.lock);
} catch (...) { /* The map's best stays unknown: no card shows one. */ }
}
bool available() noexcept { return state().ready.load(std::memory_order_acquire); }
bool enabled() noexcept { return available() && state().on.load(std::memory_order_acquire); }
void set_enabled(bool enabled) noexcept {
auto& s = state();
s.on.store(enabled, std::memory_order_release);
// Switched off, No Bail's hook hands no step on: the physics step pays nothing for it.
if (s.ready.load(std::memory_order_acquire)) set_no_bail_step_observer(enabled ? &observe_step : nullptr);
if (!enabled) {
AcquireSRWLockExclusive(&s.lock);
s.tracker.reset();
s.summary_pending = false;
ReleaseSRWLockExclusive(&s.lock);
}
profile_runtime::set_local_preference(preference, enabled);
}
Frame frame() {
auto& s = state();
if (!enabled()) return {};
const auto now = GetTickCount64();
View view;
Standing against; // the card's: the bail against the map's best before it
AcquireSRWLockExclusive(&s.lock);
view = s.tracker.view(now);
if (view.phase == Phase::bailing || view.phase == Phase::down)
against = s.best_known ? standing(s.best, view.tally.score) : Standing{};
else if (view.phase == Phase::getting_up) against = s.standing; // the best already counts this bail
ReleaseSRWLockExclusive(&s.lock);
if (view.phase == Phase::riding) return {};
Frame result;
result.card = card(view, against);
result.break_pulse = view.break_pulse;
// The skeleton as the renderer drew the skater in the latest picture, seen by its camera.
auto mesh = skeleton_mesh();
Picture picture;
PosedSkeleton posed;
if (!mesh || !latest_picture(picture) || !pose_skeleton(*mesh, picture.skin, posed)) return result;
auto& skeleton = result.skeleton;
skeleton.camera = picture.camera;
skeleton.vertical_fov = picture.vertical_fov;
skeleton.positions = std::move(posed.positions);
skeleton.normals = std::move(posed.normals);
skeleton.mesh = std::move(mesh);
skeleton.alpha = view.alpha;
skeleton.injuries = view.injuries;
skeleton.flashes = view.flashes;
return result;
}
}