#170 root-caused + fixed: the #163 SteamAPI_Shutdown was the crash -- at round end every peer's close events are ALWAYS in flight, and Shutdown tears client state down while steamclient's own service thread dispatches them (the field stack: a steamclient worker calling literal NULL, zero btl4 frames, 7 hits / 3 machines / one module-relative stack). BTSteamNet_ShutdownAll is now a QUIESCE: steamActive gated FIRST, linger closes ('round over' not timeouts), one SNS drain, 60ms grace, NO SteamAPI_Shutdown -- and every exit path dies via TerminateProcess (menu-quit no longer returns through the CRT; it uses BTOrderlyDie like everything else, so the 883 detach deadlock stays dead too). Status callback guarded for the dying window. Crash filter now names foreign DLL frames (module+offset via VirtualQuery/GetModuleFileName) so the next foreign-thread crash arrives pre-symbolized. Exit contract: steam-networking.md
This commit is contained in:
@@ -1208,6 +1208,11 @@ consistent with internals-only, see #103). Logs: `scratchpad/night16/` + Oracle
|
||||
menu-relaunch boundary. Intermittent (10 relaunches -> 3 crashes in one log). [T4 hypothesis:
|
||||
callback left registered across teardown; possibly a #163/#156 teardown-ORDER regression.]
|
||||
Players report it as "crash while joining lobby" -- it is not the join.
|
||||
**ROOT-CAUSED + FIXED same night [T2]:** the first #163 fix's `SteamAPI_Shutdown()` inside
|
||||
`BTSteamNet_ShutdownAll` raced steamclient's service thread (round-end = a burst of peer
|
||||
closes always in flight). ShutdownAll is now a QUIESCE (no Shutdown; linger closes; seam
|
||||
gated first) and every exit path dies via TerminateProcess -- see [[steam-networking]]
|
||||
§THE EXIT CONTRACT. Field verify Friday; the crash filter now names foreign DLL frames.
|
||||
- **#171 -- ballistic/missile hitscan era question (HOLD until group consensus).** Era witnesses
|
||||
(Oracle, Lynx, Sauron; Dplotta softer; Draco DISSENTS) recall AFC/Gauss as instant-hit,
|
||||
no lead, no flight animation, single-panel damage, no splash. 913 field observations: locked
|
||||
|
||||
@@ -60,6 +60,32 @@ reset on death), #69 (Sunder V1 → Denkou naming).
|
||||
token / transport layer. Everything above the seam is the same arcade mesh — the three replication
|
||||
symptoms this night produced would all reproduce on the relay.
|
||||
|
||||
## THE EXIT CONTRACT (#163 -> #170, settled 2026-08-12) [T2 field-proven]
|
||||
Process-exit topology for every glass/steam process: **flush -> quiesce -> TerminateProcess.**
|
||||
`BTSteamNet_ShutdownAll` (L4STEAMNET.cpp) is a QUIESCE: gate `steamActive` FIRST (stops
|
||||
marshal/game-thread seam re-entry), `CloseConnection(..., linger=true)` on every connection
|
||||
("round over", not a peer-side timeout), close listeners, one `sockets->RunCallbacks()` drain,
|
||||
`Sleep(60)` grace. **It deliberately never calls `SteamAPI_Shutdown()`, and no exit path may
|
||||
return through the CRT** (menu-quit included -- it dies via `BTOrderlyDie`, btl4console.cpp).
|
||||
|
||||
The two-failure history, because each fix's reason constrains the other:
|
||||
- **883 (no teardown at all):** bare `ExitProcess` ran DLL_PROCESS_DETACH with live SNS threads
|
||||
-> intermittent loader-lock deadlock -> "frozen view of how the game ended" + the menu child
|
||||
hung in `SteamAPI_Init` against the wedged half-dead sibling (#163, night15).
|
||||
- **913 (full `SteamAPI_Shutdown` before TerminateProcess):** at round end every peer closes
|
||||
simultaneously, so status events are ALWAYS in flight; Shutdown tears client state down while
|
||||
steamclient's own service thread dispatches them -> that thread calls literal NULL
|
||||
(0xc0000005 access=8 target=0x0, BaseThreadInitThunk root, ZERO btl4 frames) in the window
|
||||
before TerminateProcess (#170, night16: 7 hits / 3 machines / one module-relative stack).
|
||||
- **The stable point:** TerminateProcess kills all threads atomically with every pointer still
|
||||
valid (no detach, no dispatch window); the Steam client GCs the session on IPC pipe break --
|
||||
the standard crashed-game path (883 relaunched all night with no Shutdown ever called). The
|
||||
child-hang needed a WEDGED sibling, which TerminateProcess makes structurally impossible.
|
||||
|
||||
Forensics rider: the crash filter (btl4main.cpp `BTCrashFilter`) now prints foreign frames as
|
||||
`<module.dll>+0xOFFSET` (VirtualQuery AllocationBase + GetModuleFileName), so a future
|
||||
foreign-thread crash names its DLL instead of an unresolvable ASLR address.
|
||||
|
||||
## Key Relationships
|
||||
- Rides: [[glass-cockpit]] miniconsole (marshal has a Steam-wire branch) · Extends:
|
||||
[[multiplayer]] (the arcade mesh, unchanged above the seam)
|
||||
|
||||
@@ -155,6 +155,10 @@ static void
|
||||
static void __cdecl
|
||||
OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t *status)
|
||||
{
|
||||
if (!steamActive)
|
||||
{
|
||||
return; // #170: the quiesce window -- the process is dying;
|
||||
} // no accepts, no table writes, just survive the call
|
||||
ISteamNetworkingSockets *sockets = SteamNetworkingSockets();
|
||||
|
||||
switch (status->m_info.m_eState)
|
||||
@@ -634,7 +638,8 @@ unsigned long long
|
||||
}
|
||||
|
||||
//
|
||||
// #163 -- the ORDERED transport shutdown for process exit (2026-08-11).
|
||||
// #163/#170 -- the ORDERED transport QUIESCE for process exit
|
||||
// (2026-08-11, corrected 2026-08-12 -- see the #170 block at the tail).
|
||||
//
|
||||
// The port never called SteamAPI_Shutdown(): every round-end died via bare
|
||||
// ExitProcess with live SNS connections and Steam callback threads, and the
|
||||
@@ -655,6 +660,10 @@ void
|
||||
{
|
||||
return;
|
||||
}
|
||||
steamActive = 0; // #170 FIRST: gate every seam entry
|
||||
// (Pump/Send/Recv/Accept) against
|
||||
// re-entry from the marshal or game
|
||||
// thread while the table is torn down
|
||||
ISteamNetworkingSockets *sockets = SteamNetworkingSockets();
|
||||
for (int i = 0; i < MaxConnections; ++i)
|
||||
{
|
||||
@@ -662,7 +671,9 @@ void
|
||||
&& connections[i].connection != k_HSteamNetConnection_Invalid)
|
||||
{
|
||||
sockets->CloseConnection(connections[i].connection, 0,
|
||||
"round over", false);
|
||||
"round over", true); // #170 linger: let the close packet
|
||||
// actually transmit -- peers should
|
||||
// read "round over", not a timeout
|
||||
}
|
||||
connections[i].inUse = 0;
|
||||
}
|
||||
@@ -674,7 +685,29 @@ void
|
||||
listenSockets[p] = k_HSteamListenSocket_Invalid;
|
||||
}
|
||||
}
|
||||
SteamAPI_RunCallbacks(); // let the closes flush client-side
|
||||
steamActive = 0;
|
||||
SteamAPI_Shutdown();
|
||||
sockets->RunCallbacks(); // one SNS drain; our status callback
|
||||
// stays REGISTERED and this module
|
||||
// stays LOADED, so any late dispatch
|
||||
// lands in live (guarded) code
|
||||
Sleep(60); // bounded grace for the lingered closes
|
||||
//
|
||||
// #170 -- DELIBERATELY NO SteamAPI_Shutdown(). The first #163 fix called
|
||||
// it here, and that OVER-CORRECTED into tonight's field crash: at round
|
||||
// end every peer closes simultaneously, so ClosedByPeer/ProblemDetected
|
||||
// status events are ALWAYS in flight when this runs -- and Shutdown tears
|
||||
// the client state down while steamclient's own service thread is still
|
||||
// dispatching them. Field signature (night16, 7 hits / 3 machines): a
|
||||
// steamclient-internal thread (BaseThreadInitThunk root, ZERO btl4
|
||||
// frames) calling literal NULL (0xc0000005 access=8 target=0x0) in the
|
||||
// window between Shutdown and TerminateProcess. The contract now
|
||||
// [T2 field-proven]: quiesce only; EVERY caller must die via
|
||||
// TerminateProcess (never a CRT return -- that re-opens the DLL-detach
|
||||
// deadlock this function no longer prevents). TerminateProcess kills
|
||||
// all threads atomically with every pointer still valid; the Steam
|
||||
// client GCs the session on IPC pipe break (the standard crashed-game
|
||||
// path -- build 883 relaunched all night without a single Shutdown
|
||||
// call). The 883 child-hang-in-SteamAPI_Init needed a WEDGED
|
||||
// (detach-deadlocked, half-dead) sibling; TerminateProcess makes wedges
|
||||
// structurally impossible, so the child boots clean without Shutdown.
|
||||
//
|
||||
}
|
||||
|
||||
+34
-10
@@ -216,7 +216,28 @@ static LONG WINAPI
|
||||
if (eip >= base && eip < base + 0x2000000UL)
|
||||
std::cout << "btl4+0x" << std::hex << (eip - base);
|
||||
else
|
||||
std::cout << "0x" << std::hex << eip;
|
||||
{
|
||||
//
|
||||
// #170: name the owning MODULE -- a foreign-DLL stack is
|
||||
// unresolvable after the fact without its (ASLR'd) base; the
|
||||
// steam-worker NULL-call needed this and didn't have it.
|
||||
//
|
||||
MEMORY_BASIC_INFORMATION fmbi;
|
||||
char dll_path[MAX_PATH];
|
||||
if (VirtualQuery((void *)eip, &fmbi, sizeof(fmbi)) != 0
|
||||
&& fmbi.AllocationBase != NULL
|
||||
&& GetModuleFileNameA((HMODULE)fmbi.AllocationBase,
|
||||
dll_path, sizeof(dll_path)) != 0)
|
||||
{
|
||||
const char *dll_name = dll_path;
|
||||
for (const char *s = dll_path; *s; ++s)
|
||||
if (*s == '\\' || *s == '/') dll_name = s + 1;
|
||||
std::cout << dll_name << "+0x" << std::hex
|
||||
<< (eip - (unsigned long)fmbi.AllocationBase);
|
||||
}
|
||||
else
|
||||
std::cout << "0x" << std::hex << eip;
|
||||
}
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (ebp == 0 || (ebp & 3) != 0
|
||||
|| VirtualQuery((void *)ebp, &mbi, sizeof(mbi)) == 0
|
||||
@@ -1241,17 +1262,20 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
if (BTFrontEnd_Run(&fe_spec) != 0 || fe_spec.mode == BTFeLaunchNone)
|
||||
{
|
||||
BTLastrunNote("clean exit: player quit from the menu");
|
||||
#ifdef BT_STEAM
|
||||
// #163: the menu keeps the Steam transport up for the lobby;
|
||||
// returning through the CRT would run DLL detach with live
|
||||
// Steam threads -- the same intermittent deadlock the
|
||||
// round-end relaunch hit. Tear it down deliberately first.
|
||||
//
|
||||
// #163/#170: the menu keeps the Steam transport up for the
|
||||
// lobby, and a CRT return would run DLL detach with live
|
||||
// Steam threads (the 883 wedge). ShutdownAll is now a
|
||||
// QUIESCE -- it deliberately does NOT stop those threads
|
||||
// (SteamAPI_Shutdown mid-dispatch was the #170 NULL-call
|
||||
// crash) -- so this path must die exactly like every other
|
||||
// exit: flush, quiesce, TerminateProcess. One topology.
|
||||
//
|
||||
{
|
||||
extern void BTSteamNet_ShutdownAll();
|
||||
BTSteamNet_ShutdownAll();
|
||||
extern void BTOrderlyDie(void);
|
||||
BTOrderlyDie(); // never returns
|
||||
}
|
||||
#endif
|
||||
return 0; // quit from the menu
|
||||
return 0; // unreachable (TerminateProcess)
|
||||
}
|
||||
char fe_arguments[192];
|
||||
char fe_value[64];
|
||||
|
||||
@@ -181,9 +181,9 @@ static BOOL CALLBACK
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
BTOrderlyDie(void)
|
||||
{
|
||||
void
|
||||
BTOrderlyDie(void) // #170: non-static -- the menu-quit
|
||||
{ // path in btl4main dies through here
|
||||
{
|
||||
extern void BTMatchLogClose();
|
||||
BTMatchLogClose(); // complete receipts on disk (the
|
||||
|
||||
Reference in New Issue
Block a user