| 396 | } |
| 397 | |
| 398 | void AAMjManager::EndPlay(const EEndPlayReason::Type EndPlayReason) |
| 399 | { |
| 400 | // Stop the physics async thread BEFORE Super::EndPlay so PostStep |
| 401 | // callbacks don't race into resources child components tear down. |
| 402 | // Bounded wait: a pathological mj_step can take many seconds; an |
| 403 | // unbounded Wait() would freeze PIE-stop. On timeout we detach and |
| 404 | // leak m_model/m_data to avoid use-after-free in the still-running |
| 405 | // step (one-time per session). |
| 406 | bool bAsyncExited = true; |
| 407 | if (PhysicsEngine) |
| 408 | { |
| 409 | PhysicsEngine->bShouldStopTask = true; |
| 410 | // Wake the worker if parked on the step-request event so it |
| 411 | // observes bShouldStopTask without burning the Wait timeout. |
| 412 | if (PhysicsEngine->StepRequestEvent) |
| 413 | PhysicsEngine->StepRequestEvent->Trigger(); |
| 414 | if (PhysicsEngine->AsyncPhysicsFuture.IsValid()) |
| 415 | { |
| 416 | constexpr double kShutdownTimeoutSec = 3.0; |
| 417 | bAsyncExited = PhysicsEngine->AsyncPhysicsFuture.WaitFor( |
| 418 | FTimespan::FromSeconds(kShutdownTimeoutSec)); |
| 419 | if (!bAsyncExited) |
| 420 | { |
| 421 | UE_LOG(LogURLab, Warning, |
| 422 | TEXT("Physics async thread did not exit within %.1fs — detaching. ") |
| 423 | TEXT("mj_step is likely stuck; MuJoCo resources will leak for this session ") |
| 424 | TEXT("to avoid a use-after-free in the still-running step."), |
| 425 | kShutdownTimeoutSec); |
| 426 | } |
| 427 | } |
| 428 | PhysicsEngine->ClearCallbacks(); |
| 429 | } |
| 430 | |
| 431 | // Manager-owned transports aren't UActorComponents, so EndPlay |
| 432 | // doesn't propagate to them; explicit TransportShutdown required. |
| 433 | for (TObjectPtr<UURLabSubscribeTransport>& T : ManagerOwnedSubscribeTransports) |
| 434 | { |
| 435 | if (T) |
| 436 | T->TransportShutdown(); |
| 437 | } |
| 438 | ManagerOwnedSubscribeTransports.Reset(); |
| 439 | for (TObjectPtr<UURLabPublishTransport>& T : ManagerOwnedPublishTransports) |
| 440 | { |
| 441 | if (T) |
| 442 | T->TransportShutdown(); |
| 443 | } |
| 444 | ManagerOwnedPublishTransports.Reset(); |
| 445 | |
| 446 | Super::EndPlay(EndPlayReason); |
| 447 | if (Instance == this) |
| 448 | Instance = nullptr; |
| 449 | |
| 450 | // Manager-owned servers also tear down the dispatcher itself; |
| 451 | // subsystem-owned servers stay alive across PIE cycles. |
| 452 | if (BridgeServer) |
| 453 | { |
| 454 | BridgeServer->UnregisterManager(this); |
| 455 | if (BridgeServer->IsOwnedByManager()) |
nothing calls this directly
no test coverage detected