Round reset + device-creation retry + load-time pump (rejoin trilogy)
1. ROUND RESET (the "picked Hellbringer, got a Blackhawk" field bug): trim/prefs finalize the session egg for ONE round, and that was permanent for the relay's lifetime -- a rejoin after a mission or crash got the stale finalized egg and its new callsign/mech request was recorded but never applied. _maybe_reset_round() (both drop paths) restores the original egg/roster/launch state once every pod is gone; live beacons re-key by tag position (conn.seat_tag). Verified two-round e2e: blkhawk mission -> exit -> RESET -> lok1 request -> egg rewritten -> the Hellbringer spawned. 2. DEVICE-CREATION RETRY: CreateDevice(HARDWARE_VERTEXPROCESSING) fails transiently when a just-exited instance still holds the adapter (caught live; also the parked "exit 139" join crash). The old double-failure path called PostQuitMessage(1) -- which does NOT stop execution -- and the NULL mDevice deref right after was the real crash. Now: HW->SW retry with 500ms backoff and a 20s deadline, then a clean error box + ExitProcess. 3. LOAD-TIME PUMP: mission load is thousands of back-to-back synchronous model loads; the starved message pump ghosted the window "Not Responding" mid-load. BTLoadPump() (hooked from d3d_OBJECT::LoadObject) pumps + repaints the wait screen at most every 150ms pre-run; no-op once the scene is live. Also: round-reset originals captured after the roster parse (the first cut crashed the relay constructor -- caught by the two-round test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
638cfca18d
commit
3a9b35fc06
@@ -425,6 +425,26 @@ phase). Plan: `~/.claude/plans/partitioned-snuggling-piglet.md`.
|
||||
deferrals so a wedged load still surfaces the original Fail. `[launch] ... deferred +1s`
|
||||
log per deferral. Verified: the crashing shape deferred 104x through a ~50s load, then
|
||||
launched and ran clean.
|
||||
- **ROUND RESET (2026-07-22) [T2]**: trim/prefs FINALIZE the egg for one round -- and that used
|
||||
to be permanent for the relay's lifetime, so a REJOIN after a mission (or crash) was served
|
||||
the stale finalized egg and its new callsign/mech request was recorded but never applied (the
|
||||
field "picked Hellbringer, got a Blackhawk"). Now `_maybe_reset_round()` (triggered from both
|
||||
drop paths) restores the ORIGINAL egg/roster/launch state once every pod is gone; live beacons
|
||||
(players already waiting for the next round) re-key by their TAG's position in the restored
|
||||
roster (`conn.seat_tag`, captured at assign). Verified two-round e2e: round 1 blkhawk mission
|
||||
-> pod exit -> RESET -> round 2 requested lok1 -> egg rewritten vehicle=lok1 -> the pod
|
||||
spawned the Hellbringer. Operator flow for code updates: the relay is spawned per Start
|
||||
Session -- a session RESTART picks up new btconsole code.
|
||||
- **DEVICE-CREATION RETRY (2026-07-22) [T2]**: `CreateDevice(HARDWARE_VERTEXPROCESSING)` can
|
||||
fail TRANSIENTLY (a just-exited instance still holds the adapter -- caught live: a round-2
|
||||
pod died while round-1 tore down; also the parked "exit 139 during join"). The old code had
|
||||
no retry AND its double-failure path called PostQuitMessage(1) which does NOT stop execution
|
||||
-- the NULL mDevice deref right after was the actual crash. Now: HW->SW retry loop, 500ms
|
||||
backoff, 20s deadline, then a clean MessageBox + ExitProcess (L4VIDEO.cpp).
|
||||
- **LOAD-TIME PUMP (2026-07-22) [T2]**: the mission load is thousands of small synchronous
|
||||
model loads back-to-back -- the message pump starved and the window ghosted "Not Responding"
|
||||
mid-load. `BTLoadPump()` (L4VIDEO, hooked from `d3d_OBJECT::LoadObject`) pumps + repaints the
|
||||
wait screen at most every 150ms during pre-run states; no-op once the scene is live.
|
||||
- **PATIENT WALK-UP (2026-07-18) [T2]**: a pod dialed at a relay that isn't up yet no longer
|
||||
aborts (release `Fail()` is a bare `abort()` = looked like a "crash", field report). Both the
|
||||
seat request (`RelayRequestSeat`) and LAN discovery (`BT_RELAY=auto`) now RETRY (2s dials, 30
|
||||
|
||||
@@ -75,6 +75,9 @@ struct BTLoadTimer
|
||||
|
||||
d3d_OBJECT *d3d_OBJECT::LoadObject(LPDIRECT3DDEVICE9 device, char *fileName)
|
||||
{
|
||||
// keep the window alive through long synchronous load stretches
|
||||
extern void BTLoadPump(void);
|
||||
BTLoadPump();
|
||||
BTLoadTimer _lt(fileName);
|
||||
char fullPath[512];
|
||||
sprintf(fullPath, "VIDEO\\%s", fileName);
|
||||
|
||||
@@ -3474,15 +3474,41 @@ DPLRenderer::DPLRenderer(
|
||||
// the view; the GPU sat idle, Present <1ms). Try HARDWARE first (any modern
|
||||
// or pod GPU has fixed-function T&L); fall back to software if it fails, as
|
||||
// the original error message always intended.
|
||||
V(gD3D->CreateDevice(*mPrimaryIndex, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &mPresentParams, &mDevice));
|
||||
if (FAILED(hr))
|
||||
// DEVICE-CREATION RETRY (2026-07-22): a transient failure is REAL -- a
|
||||
// just-exited instance can still hold the adapter for a moment (caught
|
||||
// live: a round-2 pod died here while round-1 tore down). The old code
|
||||
// had TWO bugs: no retry, and on double failure PostQuitMessage(1) did
|
||||
// NOT stop execution -- the NULL mDevice deref right below was the
|
||||
// actual crash (the intermittent "exit 139" / silent join death).
|
||||
// Retry HW->SW with a short backoff; on true exhaustion exit CLEANLY
|
||||
// with a visible message.
|
||||
{
|
||||
DEBUG_STREAM<<"Couldn't create HARDWARE_VERTEXPROCESSING device."<<std::endl<<std::flush;
|
||||
|
||||
V(gD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &mPresentParams, &mDevice));
|
||||
if (FAILED(hr))
|
||||
unsigned long device_deadline = GetTickCount() + 20000UL;
|
||||
for (;;)
|
||||
{
|
||||
PostQuitMessage(1);
|
||||
hr = gD3D->CreateDevice(*mPrimaryIndex, D3DDEVTYPE_HAL, hWnd,
|
||||
D3DCREATE_HARDWARE_VERTEXPROCESSING, &mPresentParams, &mDevice);
|
||||
if (SUCCEEDED(hr) && mDevice != NULL)
|
||||
break;
|
||||
DEBUG_STREAM << "Couldn't create HARDWARE_VERTEXPROCESSING device (hr=0x"
|
||||
<< std::hex << (unsigned)hr << std::dec << ")." << std::endl << std::flush;
|
||||
hr = gD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
|
||||
D3DCREATE_SOFTWARE_VERTEXPROCESSING, &mPresentParams, &mDevice);
|
||||
if (SUCCEEDED(hr) && mDevice != NULL)
|
||||
break;
|
||||
DEBUG_STREAM << "SOFTWARE_VERTEXPROCESSING failed too (hr=0x"
|
||||
<< std::hex << (unsigned)hr << std::dec
|
||||
<< ") -- retrying in 500ms" << std::endl << std::flush;
|
||||
if (GetTickCount() >= device_deadline)
|
||||
{
|
||||
MessageBoxA(NULL,
|
||||
"Could not create a Direct3D device after 20 seconds.\n\n"
|
||||
"Another program may be holding the graphics adapter.\n"
|
||||
"Close other 3D applications and try again.",
|
||||
"BattleTech -- graphics init failed", MB_OK | MB_ICONERROR);
|
||||
ExitProcess(1); // CLEAN exit, not the NULL-deref crash
|
||||
}
|
||||
Sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8858,6 +8884,33 @@ void BTWaitScreenPaint(const char *line1, const char *line2)
|
||||
// with live game frames.
|
||||
Logical gBTSceneHasPresented = False;
|
||||
|
||||
//
|
||||
// BTLoadPump -- throttled message pump + wait-screen repaint for the LONG
|
||||
// SYNCHRONOUS stretches of mission load (thousands of small model/texture
|
||||
// loads back-to-back starve the message pump; the window ghosts "Not
|
||||
// Responding" and the animation freezes -- user report 2026-07-22). Called
|
||||
// from the model-load hot path (L4D3D LoadObject); at most one pump per
|
||||
// 150ms so the load itself is unaffected. Pre-run states only: once the
|
||||
// scene is live the render loop owns pumping and this is a no-op.
|
||||
//
|
||||
void BTLoadPump(void)
|
||||
{
|
||||
if (gBTSceneHasPresented)
|
||||
return;
|
||||
static unsigned long s_lastPump = 0;
|
||||
unsigned long now = GetTickCount();
|
||||
if (now - s_lastPump < 150UL)
|
||||
return;
|
||||
s_lastPump = now;
|
||||
MSG msg;
|
||||
while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageA(&msg);
|
||||
}
|
||||
BTWaitScreenPaint("LOADING MISSION", "stand by");
|
||||
}
|
||||
|
||||
void DPLRenderer::ExecuteIdle()
|
||||
{
|
||||
HRESULT hr;
|
||||
|
||||
@@ -313,6 +313,13 @@ class Relay:
|
||||
# meanwhile.
|
||||
self.seat_prefs = {}
|
||||
self.eggs_released = False
|
||||
# ROUND RESET originals (2026-07-22): trim/prefs FINALIZE the egg for
|
||||
# one round; when every pod leaves, the round resets to these so the
|
||||
# NEXT round's joins hold/rewrite a FRESH egg. (Field report: a
|
||||
# rejoin after a crash got the stale finalized egg -- the new mech
|
||||
# request was recorded but never applied.)
|
||||
self.orig_egg_path = egg_path
|
||||
self.orig_egg_bytes = self.egg_bytes
|
||||
# PRESENCE BEACONS (2026-07-22, "launch with whoever connects"): the
|
||||
# pod KEEPS its seat-request TCP connection open until it exits; a
|
||||
# live beacon = a seated player. A beacon dropping before the seat
|
||||
@@ -320,6 +327,7 @@ class Relay:
|
||||
# LAUNCH trims the roster to the seats present.
|
||||
self.seat_beacons = {}
|
||||
self.roster = parse_egg_roster(egg_path)
|
||||
self.orig_roster = list(self.roster) # round-reset restore point
|
||||
self.expected_ids = set(range(FIRST_GAME_HOST_ID,
|
||||
FIRST_GAME_HOST_ID + len(self.roster)))
|
||||
# OPERATOR-RESERVED SEATS: roster seats the operator flies LOCALLY (with
|
||||
@@ -545,6 +553,46 @@ class Relay:
|
||||
print(f"[relay] seat-pref egg rewrite FAILED ({e!r}) -- "
|
||||
f"serving the original egg", flush=True)
|
||||
|
||||
def _maybe_reset_round(self):
|
||||
# All pods gone from a FINALIZED round -> restore the template so the
|
||||
# next round's joins are honored (held egg, fresh prefs/trim). Live
|
||||
# beacons (players already waiting for the next round) are re-keyed
|
||||
# by their TAG's position in the restored roster.
|
||||
if not self.eggs_released:
|
||||
return
|
||||
if self.by_host or any(c.acked for c in self.console_conns):
|
||||
return
|
||||
self.eggs_released = False
|
||||
self.egg_path = self.orig_egg_path
|
||||
self.egg_bytes = self.orig_egg_bytes
|
||||
self.roster = list(self.orig_roster)
|
||||
self.expected_ids = set(range(FIRST_GAME_HOST_ID,
|
||||
FIRST_GAME_HOST_ID + len(self.roster)))
|
||||
self.launches_sent = 0
|
||||
self.launch_at = None
|
||||
self.launch_requested = False
|
||||
self.eggs_done_at = None
|
||||
self._launch_blocked_warned = False
|
||||
self.stop_sent = False
|
||||
self.stop_requested = False
|
||||
self.mission_started_at = None
|
||||
old_beacons = self.seat_beacons
|
||||
old_prefs = self.seat_prefs
|
||||
self.seat_beacons = {}
|
||||
self.seat_prefs = {}
|
||||
self.seat_reservations = {}
|
||||
for old_id, conn in old_beacons.items():
|
||||
tag = getattr(conn, "seat_tag", None)
|
||||
if tag in self.roster:
|
||||
new_id = FIRST_GAME_HOST_ID + self.roster.index(tag)
|
||||
conn.seat_host = new_id
|
||||
self.seat_beacons[new_id] = conn
|
||||
if old_id in old_prefs:
|
||||
self.seat_prefs[new_id] = old_prefs[old_id]
|
||||
print(f"[relay] round RESET -- roster/egg restored "
|
||||
f"({len(self.seat_beacons)} player(s) already waiting)",
|
||||
flush=True)
|
||||
|
||||
def _pods_ready(self):
|
||||
"""REAL pods = console connections that ACKED the egg (scanners never
|
||||
speak the protocol, so they can't hold a roster slot)."""
|
||||
@@ -620,6 +668,7 @@ class Relay:
|
||||
pass
|
||||
if conn in self.console_conns:
|
||||
self.console_conns.remove(conn)
|
||||
self._maybe_reset_round()
|
||||
|
||||
def _log_launch_readiness(self):
|
||||
# PRE-LAUNCH READINESS (2026-07-18): make the "launching short" footgun
|
||||
@@ -834,6 +883,7 @@ class Relay:
|
||||
self._send_raw(conn, struct.pack(ENV_FMT_TCP, ROUTE_SEAT_ASSIGN,
|
||||
len(payload_out)) + payload_out)
|
||||
conn.seat_host = host_id # this conn IS the beacon now
|
||||
conn.seat_tag = tag # tag survives a round reset
|
||||
self.seat_beacons[host_id] = conn
|
||||
print(f"[relay] {conn.name()} seat request -> assigned host "
|
||||
f"{host_id} '{tag}' (reserved {SEAT_RESERVE_SECONDS:.0f}s)"
|
||||
@@ -929,6 +979,7 @@ class Relay:
|
||||
self.udp_endpoint.pop(conn.host_id, None)
|
||||
for other in self.by_host.values():
|
||||
self._send_control(other, ROUTE_PEER_DOWN, conn.host_id)
|
||||
self._maybe_reset_round()
|
||||
|
||||
# ---------------- game UDP side ----------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user