mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-30 19:23:20 +08:00
714 lines
25 KiB
C++
714 lines
25 KiB
C++
// Released under the MIT License. See LICENSE for details.
|
|
|
|
#include "ballistica/base/logic/logic.h"
|
|
|
|
#include <Python.h>
|
|
|
|
#include "ballistica/base/app_adapter/app_adapter.h"
|
|
#include "ballistica/base/app_mode/app_mode.h"
|
|
#include "ballistica/base/audio/audio.h"
|
|
#include "ballistica/base/input/input.h"
|
|
#include "ballistica/base/networking/networking.h"
|
|
#include "ballistica/base/platform/base_platform.h"
|
|
#include "ballistica/base/python/base_python.h"
|
|
#include "ballistica/base/support/context.h"
|
|
#include "ballistica/base/support/plus_soft.h"
|
|
#include "ballistica/base/support/stdio_console.h"
|
|
#include "ballistica/base/ui/dev_console.h"
|
|
#include "ballistica/base/ui/ui.h"
|
|
#include "ballistica/shared/foundation/event_loop.h"
|
|
|
|
namespace ballistica::base {
|
|
|
|
Logic::Logic() : display_timers_(new TimerList()) {
|
|
// Enable display-time debug logs via env var.
|
|
auto val = g_core->platform->GetEnv("BA_DEBUG_LOG_DISPLAY_TIME");
|
|
if (val && *val == "1") {
|
|
debug_log_display_time_ = true;
|
|
}
|
|
}
|
|
|
|
void Logic::OnMainThreadStartApp() {
|
|
// Spin up our logic thread and sit and wait for it to init.
|
|
event_loop_ = new EventLoop(EventLoopID::kLogic);
|
|
g_core->suspendable_event_loops.push_back(event_loop_);
|
|
event_loop_->PushCallSynchronous([this] { OnAppStart(); });
|
|
}
|
|
|
|
void Logic::OnAppStart() {
|
|
assert(g_base->InLogicThread());
|
|
g_core->LifecycleLog("on-app-start begin (logic thread)");
|
|
|
|
// Our thread should not be holding the GIL here at the start (and
|
|
// probably will not have any Python state at all). So here we set both
|
|
// of those up.
|
|
assert(!PyGILState_Check());
|
|
PyGILState_Ensure();
|
|
|
|
// Code running in the logic thread holds the GIL by default.
|
|
event_loop_->SetAcquiresPythonGIL();
|
|
|
|
// Stay informed when our event loop is pausing/unpausing.
|
|
event_loop_->AddSuspendCallback(
|
|
NewLambdaRunnableUnmanaged([this] { OnAppSuspend(); }));
|
|
event_loop_->AddUnsuspendCallback(
|
|
NewLambdaRunnableUnmanaged([this] { OnAppUnsuspend(); }));
|
|
|
|
// Running in a specific order here and should try to stick to it in
|
|
// other OnAppXXX callbacks so any subsystem interdependencies behave
|
|
// consistently. When pausing or shutting-down we use the opposite order for
|
|
// the same reason. Let's do Python last (or first when pausing, etc) since
|
|
// it will be the most variable; that way it will interact with other
|
|
// subsystems in their normal states which is less likely to lead to
|
|
// problems.
|
|
g_base->app_adapter->OnAppStart();
|
|
g_base->platform->OnAppStart();
|
|
g_base->graphics->OnAppStart();
|
|
g_base->audio->OnAppStart();
|
|
g_base->input->OnAppStart();
|
|
g_base->ui->OnAppStart();
|
|
g_base->app_mode()->OnAppStart();
|
|
if (g_base->HavePlus()) {
|
|
g_base->plus()->OnAppStart();
|
|
}
|
|
g_base->python->OnAppStart();
|
|
|
|
g_core->LifecycleLog("on-app-start end (logic thread)");
|
|
}
|
|
|
|
void Logic::OnGraphicsReady() {
|
|
assert(g_base->InLogicThread());
|
|
if (graphics_ready_) {
|
|
// Only want to fire this logic the first time.
|
|
return;
|
|
}
|
|
graphics_ready_ = true;
|
|
|
|
// Ok; graphics-server is telling us we've got a screen (or no screen in
|
|
// the case of headless-mode). We use this as a cue to kick off our
|
|
// business logic.
|
|
|
|
// Let the Python layer know the native layer is now fully functional.
|
|
// This will probably result in the Python layer flipping to the INITING
|
|
// state.
|
|
CompleteAppBootstrapping_();
|
|
|
|
if (g_core->HeadlessMode()) {
|
|
// Normally we step display-time as part of our frame-drawing process.
|
|
// If we're headless, we're not drawing any frames, but we still want to
|
|
// do minimal processing on any display-time timers so code doesn't
|
|
// break. Let's run at a low-ish rate (10hz) to keep things efficient.
|
|
// Anyone dealing in display-time should be able to handle a wide
|
|
// variety of rates anyway. NOTE: This length is currently milliseconds.
|
|
headless_display_time_step_timer_ = event_loop()->NewTimer(
|
|
kHeadlessMinDisplayTimeStep, true,
|
|
NewLambdaRunnable([this] { StepDisplayTime_(); }).Get());
|
|
} else {
|
|
// In gui mode, push an initial frame to the graphics server. From this
|
|
// point it will be self-sustaining, sending us a frame request each
|
|
// time it receives a new frame from us.
|
|
g_base->graphics->BuildAndPushFrameDef();
|
|
}
|
|
}
|
|
|
|
void Logic::CompleteAppBootstrapping_() {
|
|
assert(g_base->InLogicThread());
|
|
assert(g_base->CurrentContext().IsEmpty());
|
|
|
|
assert(!app_bootstrapping_complete_);
|
|
app_bootstrapping_complete_ = true;
|
|
|
|
g_core->LifecycleLog("app native bootstrapping complete");
|
|
|
|
// Let the assets system know it can start loading stuff now that
|
|
// we have a screen and thus know texture formats/etc.
|
|
// TODO(ericf): It might be nice to kick this off earlier if our logic is
|
|
// robust enough to create some sort of 'null' textures/meshes before
|
|
// the renderer is ready and then seamlessly create renderer-specific
|
|
// ones once the renderer is up. We could likely at least get a lot
|
|
// of preloads done in the meantime. Though this would require preloads
|
|
// to be renderer-agnostic; not sure if that will always be the case.
|
|
g_base->assets->StartLoading();
|
|
|
|
// Let base know it can create the console or other asset-dependent things.
|
|
g_base->OnAssetsAvailable();
|
|
|
|
// Set up our timers.
|
|
process_pending_work_timer_ = event_loop()->NewTimer(
|
|
0, true, NewLambdaRunnable([this] { ProcessPendingWork_(); }).Get());
|
|
// asset_prune_timer_ = event_loop()->NewTimer(
|
|
// 2345 * 1000, true, NewLambdaRunnable([] { g_base->assets->Prune();
|
|
// }).Get());
|
|
|
|
// Let our initial dummy app-mode know it has become active.
|
|
g_base->app_mode()->OnActivate();
|
|
|
|
// Reset our various subsystems to a default state.
|
|
g_base->ui->Reset();
|
|
g_base->input->Reset();
|
|
g_base->graphics->Reset();
|
|
g_base->python->Reset();
|
|
g_base->audio->Reset();
|
|
|
|
// Let Python know we're done bootstrapping so it can flip the app
|
|
// into the 'launching' state.
|
|
g_base->python->objs()
|
|
.Get(BasePython::ObjID::kAppOnNativeBootstrappingCompleteCall)
|
|
.Call();
|
|
|
|
UpdatePendingWorkTimer_();
|
|
}
|
|
|
|
void Logic::OnAppRunning() {
|
|
assert(g_base->InLogicThread());
|
|
assert(g_base->CurrentContext().IsEmpty());
|
|
|
|
// Currently don't do anything here.
|
|
}
|
|
|
|
void Logic::OnInitialAppModeSet() {
|
|
assert(g_base->InLogicThread());
|
|
assert(g_base->CurrentContext().IsEmpty());
|
|
|
|
// We want any sort of raw Python input to only start accepting commands
|
|
// once we've got an initial app-mode set. Generally said commands will
|
|
// assume we're running in that mode and will fail if run before it is set.
|
|
if (auto* console = g_base->ui->dev_console()) {
|
|
console->EnableInput();
|
|
}
|
|
if (g_base->stdio_console) {
|
|
g_base->stdio_console->Start();
|
|
}
|
|
}
|
|
|
|
void Logic::OnAppSuspend() {
|
|
assert(g_base->InLogicThread());
|
|
assert(g_base->CurrentContext().IsEmpty());
|
|
|
|
// Note: keep these in opposite order of OnAppStart.
|
|
g_base->python->OnAppSuspend();
|
|
if (g_base->HavePlus()) {
|
|
g_base->plus()->OnAppSuspend();
|
|
}
|
|
g_base->app_mode()->OnAppSuspend();
|
|
g_base->ui->OnAppSuspend();
|
|
g_base->input->OnAppSuspend();
|
|
g_base->audio->OnAppSuspend();
|
|
g_base->graphics->OnAppSuspend();
|
|
g_base->platform->OnAppSuspend();
|
|
g_base->app_adapter->OnAppSuspend();
|
|
}
|
|
|
|
void Logic::OnAppUnsuspend() {
|
|
assert(g_base->InLogicThread());
|
|
assert(g_base->CurrentContext().IsEmpty());
|
|
|
|
// Note: keep these in the same order as OnAppStart.
|
|
g_base->app_adapter->OnAppUnsuspend();
|
|
g_base->platform->OnAppUnsuspend();
|
|
g_base->graphics->OnAppUnsuspend();
|
|
g_base->audio->OnAppUnsuspend();
|
|
g_base->input->OnAppUnsuspend();
|
|
g_base->ui->OnAppUnsuspend();
|
|
g_base->app_mode()->OnAppUnsuspend();
|
|
if (g_base->HavePlus()) {
|
|
g_base->plus()->OnAppUnsuspend();
|
|
}
|
|
g_base->python->OnAppUnsuspend();
|
|
}
|
|
|
|
void Logic::Shutdown() {
|
|
assert(g_base->InLogicThread());
|
|
assert(g_base->IsAppStarted());
|
|
|
|
if (!shutting_down_) {
|
|
shutting_down_ = true;
|
|
OnAppShutdown();
|
|
}
|
|
}
|
|
|
|
void Logic::OnAppShutdown() {
|
|
assert(g_core);
|
|
assert(g_base->CurrentContext().IsEmpty());
|
|
assert(shutting_down_);
|
|
|
|
// Nuke the app from orbit if we get stuck while shutting down.
|
|
g_core->StartSuicideTimer("shutdown", 10000);
|
|
|
|
// Tell base to disallow shutdown-suppressors from here on out.
|
|
g_base->ShutdownSuppressDisallow();
|
|
|
|
// Let our logic thread subsystems know we're shutting down.
|
|
// Note: Keep these in opposite order of OnAppStart.
|
|
// Note2: Any shutdown processes that take a non-zero amount of time
|
|
// should be registered as shutdown-tasks
|
|
g_base->python->OnAppShutdown();
|
|
if (g_base->HavePlus()) {
|
|
g_base->plus()->OnAppShutdown();
|
|
}
|
|
g_base->app_mode()->OnAppShutdown();
|
|
g_base->ui->OnAppShutdown();
|
|
g_base->input->OnAppShutdown();
|
|
g_base->audio->OnAppShutdown();
|
|
g_base->graphics->OnAppShutdown();
|
|
g_base->platform->OnAppShutdown();
|
|
g_base->app_adapter->OnAppShutdown();
|
|
}
|
|
|
|
void Logic::CompleteShutdown() {
|
|
BA_PRECONDITION(g_base->InLogicThread());
|
|
BA_PRECONDITION(shutting_down_);
|
|
BA_PRECONDITION(!shutdown_completed_);
|
|
|
|
shutdown_completed_ = true;
|
|
OnAppShutdownComplete();
|
|
}
|
|
|
|
void Logic::OnAppShutdownComplete() {
|
|
assert(g_base->InLogicThread());
|
|
|
|
// Wrap up any last business here in the logic thread and then kick things
|
|
// over to the main thread to exit out of the main loop.
|
|
|
|
// Let our logic subsystems know in case there's any last thing they'd
|
|
// like to do right before we exit.
|
|
// Note: Keep these in opposite order of OnAppStart.
|
|
// Note2: Any shutdown processes that take a non-zero amount of time
|
|
// should be registered as shutdown-tasks.
|
|
g_base->python->OnAppShutdownComplete();
|
|
if (g_base->HavePlus()) {
|
|
g_base->plus()->OnAppShutdownComplete();
|
|
}
|
|
g_base->app_mode()->OnAppShutdownComplete();
|
|
g_base->ui->OnAppShutdownComplete();
|
|
g_base->input->OnAppShutdownComplete();
|
|
g_base->audio->OnAppShutdownComplete();
|
|
g_base->graphics->OnAppShutdownComplete();
|
|
g_base->platform->OnAppShutdownComplete();
|
|
g_base->app_adapter->OnAppShutdownComplete();
|
|
|
|
g_base->app_adapter->PushMainThreadCall(
|
|
[] { g_base->OnAppShutdownComplete(); });
|
|
}
|
|
|
|
void Logic::DoApplyAppConfig() {
|
|
assert(g_base->InLogicThread());
|
|
|
|
// Give all our other subsystems a chance.
|
|
// Note: keep these in the same order as OnAppStart.
|
|
g_base->app_adapter->DoApplyAppConfig();
|
|
g_base->platform->DoApplyAppConfig();
|
|
g_base->graphics->DoApplyAppConfig();
|
|
g_base->audio->DoApplyAppConfig();
|
|
g_base->input->DoApplyAppConfig();
|
|
g_base->ui->DoApplyAppConfig();
|
|
g_base->app_mode()->DoApplyAppConfig();
|
|
if (g_base->HavePlus()) {
|
|
g_base->plus()->DoApplyAppConfig();
|
|
}
|
|
g_base->python->DoApplyAppConfig();
|
|
|
|
// Inform some other subsystems even though they're not our standard
|
|
// set of logic-thread-based ones.
|
|
g_base->networking->DoApplyAppConfig();
|
|
|
|
applied_app_config_ = true;
|
|
}
|
|
|
|
void Logic::OnScreenSizeChange(float virtual_width, float virtual_height,
|
|
float pixel_width, float pixel_height) {
|
|
assert(g_base->InLogicThread());
|
|
|
|
// Inform all subsystems.
|
|
// Note: keep these in the same order as OnAppStart.
|
|
g_base->app_adapter->OnScreenSizeChange();
|
|
g_base->platform->OnScreenSizeChange();
|
|
g_base->graphics->OnScreenSizeChange();
|
|
g_base->audio->OnScreenSizeChange();
|
|
g_base->input->OnScreenSizeChange();
|
|
g_base->ui->OnScreenSizeChange();
|
|
g_core->platform->OnScreenSizeChange();
|
|
g_base->app_mode()->OnScreenSizeChange();
|
|
if (g_base->HavePlus()) {
|
|
g_base->plus()->OnScreenSizeChange();
|
|
}
|
|
g_base->python->OnScreenSizeChange();
|
|
}
|
|
|
|
// Bring all logic-thread stuff up to date for a new visual frame.
|
|
void Logic::StepDisplayTime_() {
|
|
assert(g_base->InLogicThread());
|
|
|
|
// We have two different modes of operation here. When running in headless
|
|
// mode, display time is driven by upcoming events such as sim steps; we
|
|
// basically want to sleep as long as we can and run steps exactly when
|
|
// events occur. When running with a gui, our display-time is driven by
|
|
// real draw times and is intended to keep frame intervals as visually
|
|
// consistent and smooth looking as possible.
|
|
if (g_core->HeadlessMode()) {
|
|
UpdateDisplayTimeForHeadlessMode_();
|
|
} else {
|
|
UpdateDisplayTimeForFrameDraw_();
|
|
}
|
|
|
|
// Give all our subsystems some update love.
|
|
// Note: keep these in the same order as OnAppStart.
|
|
g_base->graphics->StepDisplayTime();
|
|
g_base->audio->StepDisplayTime();
|
|
g_base->input->StepDisplayTime();
|
|
g_base->ui->StepDisplayTime();
|
|
g_core->platform->StepDisplayTime();
|
|
g_base->app_mode()->StepDisplayTime();
|
|
if (g_base->HavePlus()) {
|
|
g_base->plus()->StepDisplayTime();
|
|
}
|
|
g_base->python->StepDisplayTime();
|
|
|
|
// Let's run display-timers *after* we step everything else so most things
|
|
// they interact with will be in an up-to-date state.
|
|
display_timers_->Run(display_time_microsecs_);
|
|
|
|
if (g_core->HeadlessMode()) {
|
|
PostUpdateDisplayTimeForHeadlessMode_();
|
|
}
|
|
}
|
|
|
|
void Logic::OnAppModeChanged() {
|
|
assert(g_base->InLogicThread());
|
|
|
|
// Kick our headless stepping into high gear; this will snap us out of any
|
|
// long sleep we're currently in the middle of.
|
|
if (g_core->HeadlessMode()) {
|
|
if (debug_log_display_time_) {
|
|
Log(LogLevel::kDebug,
|
|
"Resetting headless display step timer due to app-mode change.");
|
|
}
|
|
assert(headless_display_time_step_timer_);
|
|
headless_display_time_step_timer_->SetLength(kHeadlessMinDisplayTimeStep);
|
|
}
|
|
}
|
|
|
|
void Logic::UpdateDisplayTimeForHeadlessMode_() {
|
|
assert(g_base->InLogicThread());
|
|
// In this case we just keep display time synced up with app time; we
|
|
// don't care about keeping the increments smooth or consistent.
|
|
|
|
// The one thing we *do* try to do, however, is keep our timer length
|
|
// updated so that we fire exactly when the next app-mode event is
|
|
// scheduled (or at least close enough so we can fudge it and tell them
|
|
// its that exact time).
|
|
|
|
auto app_time_microsecs = g_core->GetAppTimeMicrosecs();
|
|
|
|
// Set our int based time vals so we can exactly hit timers.
|
|
auto old_display_time_microsecs = display_time_microsecs_;
|
|
display_time_microsecs_ = app_time_microsecs;
|
|
display_time_increment_microsecs_ =
|
|
display_time_microsecs_ - old_display_time_microsecs;
|
|
|
|
// And then our float time vals are driven by our int ones.
|
|
display_time_ = static_cast<double>(display_time_microsecs_) / 1000000.0;
|
|
display_time_increment_ =
|
|
static_cast<double>(display_time_increment_microsecs_) / 1000000.0;
|
|
|
|
if (debug_log_display_time_) {
|
|
char buffer[256];
|
|
snprintf(buffer, sizeof(buffer), "stepping display-time at app-time %.4f",
|
|
static_cast<double>(app_time_microsecs) / 1000000.0);
|
|
Log(LogLevel::kDebug, buffer);
|
|
}
|
|
}
|
|
|
|
void Logic::PostUpdateDisplayTimeForHeadlessMode_() {
|
|
assert(g_base->InLogicThread());
|
|
// At this point we've stepped our app-mode, so let's ask it how long
|
|
// we've got until the next event. We'll plug this into our display-update
|
|
// timer so we can try to sleep exactly until that point.
|
|
auto headless_display_step_microsecs =
|
|
std::max(std::min(g_base->app_mode()->GetHeadlessNextDisplayTimeStep(),
|
|
kHeadlessMaxDisplayTimeStep),
|
|
kHeadlessMinDisplayTimeStep);
|
|
|
|
if (debug_log_display_time_) {
|
|
auto sleepsecs =
|
|
static_cast<double>(headless_display_step_microsecs) / 1000000.0;
|
|
auto apptimesecs = g_core->GetAppTimeSeconds();
|
|
char buffer[256];
|
|
snprintf(buffer, sizeof(buffer),
|
|
"will try to sleep for %.4f at app-time %.4f (until %.4f)",
|
|
sleepsecs, apptimesecs, apptimesecs + sleepsecs);
|
|
Log(LogLevel::kDebug, buffer);
|
|
}
|
|
|
|
auto sleep_microsecs = headless_display_step_microsecs;
|
|
headless_display_time_step_timer_->SetLength(sleep_microsecs);
|
|
}
|
|
|
|
void Logic::UpdateDisplayTimeForFrameDraw_() {
|
|
// Here we update our smoothed display-time-increment based on how fast we
|
|
// are currently rendering frames. We want display-time to basically be
|
|
// progressing at the same rate as app-time but in as constant of a manner
|
|
// as possible so that animation, simulation-stepping/etc. appears smooth
|
|
// (using app-times within renders exhibits quite a bit of jitter). Though
|
|
// we also don't want it to be *too* smooth; drops in framerate should
|
|
// still be reflected quickly in display-time-increment otherwise it can
|
|
// look like the game is slowing down or speeding up.
|
|
|
|
// Flip debug-log-display-time on to debug this stuff.
|
|
// Things to look for:
|
|
// - 'final' value should mostly stay constant.
|
|
// - 'final' value should not be *too* far from 'current'.
|
|
// - 'current' should mostly show '(avg)'; rarely '(sample)'.
|
|
// - these can vary briefly during load spikes/etc. but should quickly
|
|
// reconverge to stability. If not, this may need further calibration.
|
|
auto current_app_time = g_core->GetAppTimeSeconds();
|
|
|
|
// We handle the first measurement specially.
|
|
if (last_display_time_update_app_time_ < 0) {
|
|
last_display_time_update_app_time_ = current_app_time;
|
|
} else {
|
|
auto this_increment = current_app_time - last_display_time_update_app_time_;
|
|
last_display_time_update_app_time_ = current_app_time;
|
|
|
|
// Store increments into a looping buffer.
|
|
if (recent_display_time_increments_index_ < 0) {
|
|
// For the first sample we fill all entries.
|
|
for (auto& recent_display_time_increment :
|
|
recent_display_time_increments_) {
|
|
recent_display_time_increment = this_increment;
|
|
}
|
|
recent_display_time_increments_index_ = 0;
|
|
} else {
|
|
recent_display_time_increments_[recent_display_time_increments_index_] =
|
|
this_increment;
|
|
recent_display_time_increments_index_ =
|
|
(recent_display_time_increments_index_ + 1) % kDisplayTimeSampleCount;
|
|
}
|
|
|
|
double avg{};
|
|
double min, max;
|
|
min = max = recent_display_time_increments_[0];
|
|
for (int i = 0; i < kDisplayTimeSampleCount; ++i) {
|
|
auto val = recent_display_time_increments_[i];
|
|
avg += val;
|
|
min = std::min(min, val);
|
|
max = std::max(max, val);
|
|
}
|
|
avg /= kDisplayTimeSampleCount;
|
|
double range = max - min;
|
|
|
|
// If our range of recent increment values is somewhat large relative to
|
|
// an average value, things are probably chaotic, so just use the
|
|
// current value to respond quickly to changes. If things are more calm,
|
|
// use our nice smoothed value.
|
|
|
|
// Let's use 1.0 as a final 'chaos' threshold to make logs easy to read.
|
|
// So our key fudge factor here is chaos_fudge. The higher this value,
|
|
// the lower chaos will be and thus the more the engine will stick to
|
|
// smoothed values. A good way to determine if this value is too high is
|
|
// to launch the game and watch the menu animation. If it visibly speeds
|
|
// up or slows down in a 'rubber band' looking way the moment after
|
|
// launch, it means the value is too high and the engine is sticking
|
|
// with smoothed values when it should instead be reacting immediately.
|
|
// So basically this value should be as high as possible while avoiding
|
|
// that look.
|
|
double chaos_fudge{1.25};
|
|
double chaos = (range / avg) / chaos_fudge;
|
|
bool use_avg = chaos < 1.0;
|
|
auto used = use_avg ? avg : this_increment;
|
|
|
|
// Lastly use this 'used' value to update our actual increment - our
|
|
// increment moves only if 'used' value gets farther than [trail_buffer]
|
|
// from it. So ideally it will sit in the middle of the smoothed value
|
|
// range.
|
|
|
|
// How far the smoothed increment value needs to get away from the
|
|
// current smooth value to actually start moving it. Example: If our
|
|
// smooth increment is 16.6ms (60fps), don't change our increment until
|
|
// the 'used' value is more than 0.5ms (16.6 * 0.03) from it in either
|
|
// direction.
|
|
|
|
// Note: In practice I'm seeing that higher framerates like 120 need
|
|
// buffers that are larger relative to avg to remain stable. Though
|
|
// perhaps a bit of jitter is not noticeable at high frame rates; just
|
|
// something to keep an eye on.
|
|
auto trail_buffer{avg * 0.03};
|
|
|
|
auto trailing_diff = used - display_time_increment_;
|
|
auto trailing_dist = std::abs(trailing_diff);
|
|
if (trailing_dist > trail_buffer) {
|
|
auto offs =
|
|
(trailing_dist - trail_buffer) * (trailing_diff > 0.0 ? 1.0 : -1.0);
|
|
if (debug_log_display_time_) {
|
|
char buffer[256];
|
|
snprintf(buffer, sizeof(buffer),
|
|
"trailing_dist %.6f > trail_buffer %.6f; will offset %.6f).",
|
|
trailing_dist, trail_buffer, offs);
|
|
Log(LogLevel::kDebug, buffer);
|
|
}
|
|
display_time_increment_ = display_time_increment_ + offs;
|
|
}
|
|
|
|
if (debug_log_display_time_) {
|
|
char buffer[256];
|
|
snprintf(buffer, sizeof(buffer),
|
|
"final %.5f current(%s) %.5f sample %.5f chaos %.5f",
|
|
display_time_increment_, use_avg ? "avg" : "sample", used,
|
|
this_increment, chaos);
|
|
Log(LogLevel::kDebug, buffer);
|
|
}
|
|
}
|
|
|
|
// Lastly, apply our updated increment value to our time.
|
|
display_time_ += display_time_increment_;
|
|
|
|
// In this path, our integer values just follow our float ones.
|
|
auto prev_microsecs = display_time_microsecs_;
|
|
display_time_microsecs_ = static_cast<microsecs_t>(display_time_ * 1000000.0);
|
|
display_time_increment_microsecs_ = display_time_microsecs_ - prev_microsecs;
|
|
}
|
|
|
|
// Set up our sleeping based on what we're doing.
|
|
void Logic::UpdatePendingWorkTimer_() {
|
|
assert(g_base->InLogicThread());
|
|
|
|
// This might get called before we set up our timer in some cases. (such
|
|
// as very early) should be safe to ignore since we update the interval
|
|
// explicitly after creating the timers.
|
|
if (!process_pending_work_timer_) {
|
|
return;
|
|
}
|
|
|
|
// If there's loading to do, keep at it rather vigorously.
|
|
if (have_pending_loads_) {
|
|
assert(process_pending_work_timer_);
|
|
process_pending_work_timer_->SetLength(1 * 1000);
|
|
} else {
|
|
// Otherwise we've got nothing to do; go to sleep until something
|
|
// changes.
|
|
assert(process_pending_work_timer_);
|
|
process_pending_work_timer_->SetLength(-1);
|
|
}
|
|
}
|
|
|
|
void Logic::HandleInterruptSignal() {
|
|
assert(g_base->InLogicThread());
|
|
|
|
// Interrupt signals are 'gentle' requests to shut down.
|
|
|
|
// Special case; when running under the server-wrapper, we completely
|
|
// ignore interrupt signals (the wrapper acts on them).
|
|
if (g_base->server_wrapper_managed()) {
|
|
return;
|
|
}
|
|
Shutdown();
|
|
}
|
|
|
|
void Logic::HandleTerminateSignal() {
|
|
// Interrupt signals are slightly more stern requests to shut down.
|
|
// We always respond to these.
|
|
assert(g_base->InLogicThread());
|
|
Shutdown();
|
|
}
|
|
|
|
void Logic::Draw() {
|
|
assert(g_base->InLogicThread());
|
|
assert(!g_core->HeadlessMode());
|
|
|
|
// Push a snapshot of our current state to be rendered in the graphics
|
|
// thread.
|
|
g_base->graphics->BuildAndPushFrameDef();
|
|
|
|
// Now bring logic up to date. By doing this *after* fulfilling the draw
|
|
// request, we're minimizing the chance of long logic updates leading to
|
|
// delays in frame-def delivery leading to frame drops. The downside is
|
|
// that when logic updates are fast then logic is basically sitting around
|
|
// twiddling its thumbs and getting a full frame out of date before being
|
|
// drawn. But as high frame rates are becoming more normal this becomes
|
|
// less and less meaningful and its probably best to prioritize smooth
|
|
// visuals.
|
|
StepDisplayTime_();
|
|
}
|
|
|
|
void Logic::NotifyOfPendingAssetLoads() {
|
|
assert(g_base->InLogicThread());
|
|
have_pending_loads_ = true;
|
|
UpdatePendingWorkTimer_();
|
|
}
|
|
|
|
auto Logic::NewAppTimer(microsecs_t length, bool repeat, Runnable* runnable)
|
|
-> int {
|
|
// App-Timers simply get injected into our loop and run alongside our own
|
|
// stuff.
|
|
assert(g_base->InLogicThread());
|
|
auto* timer = event_loop()->NewTimer(length, repeat, runnable);
|
|
return timer->id();
|
|
}
|
|
|
|
void Logic::DeleteAppTimer(int timer_id) {
|
|
assert(g_base->InLogicThread());
|
|
event_loop()->DeleteTimer(timer_id);
|
|
}
|
|
|
|
void Logic::SetAppTimerLength(int timer_id, microsecs_t length) {
|
|
assert(g_base->InLogicThread());
|
|
Timer* t = event_loop()->GetTimer(timer_id);
|
|
if (t) {
|
|
t->SetLength(length);
|
|
} else {
|
|
Log(LogLevel::kError,
|
|
"Logic::SetAppTimerLength() called on nonexistent timer.");
|
|
}
|
|
}
|
|
|
|
auto Logic::NewDisplayTimer(microsecs_t length, bool repeat, Runnable* runnable)
|
|
-> int {
|
|
// Display-Timers go into a timer-list that we exec explicitly when we
|
|
// step display-time.
|
|
assert(g_base->InLogicThread());
|
|
int offset = 0;
|
|
Timer* t = display_timers_->NewTimer(display_time_microsecs_, length, offset,
|
|
repeat ? -1 : 0, runnable);
|
|
return t->id();
|
|
}
|
|
|
|
void Logic::DeleteDisplayTimer(int timer_id) {
|
|
assert(g_base->InLogicThread());
|
|
display_timers_->DeleteTimer(timer_id);
|
|
}
|
|
|
|
void Logic::SetDisplayTimerLength(int timer_id, microsecs_t length) {
|
|
assert(g_base->InLogicThread());
|
|
Timer* t = display_timers_->GetTimer(timer_id);
|
|
if (t) {
|
|
t->SetLength(length);
|
|
} else {
|
|
Log(LogLevel::kError,
|
|
"Logic::SetDisplayTimerLength() called on nonexistent timer.");
|
|
}
|
|
}
|
|
|
|
void Logic::ProcessPendingWork_() {
|
|
have_pending_loads_ = g_base->assets->RunPendingLoadsLogicThread();
|
|
UpdatePendingWorkTimer_();
|
|
}
|
|
|
|
void Logic::OnAppActiveChanged() {
|
|
assert(g_base->InLogicThread());
|
|
|
|
// Note: we keep our own active state here in the logic thread and
|
|
// simply refresh it from the atomic value from the main thread here.
|
|
// There are occasions where the main thread's value flip-flops back
|
|
// and forth quickly and we'll generally skip over those this way.
|
|
auto app_active = g_base->app_active();
|
|
if (app_active != app_active_) {
|
|
app_active_ = app_active;
|
|
|
|
// For now just informing Python (which informs Python level app-mode).
|
|
// Can expand this to inform everyone else if needed.
|
|
g_base->python->OnAppActiveChanged();
|
|
}
|
|
}
|
|
|
|
} // namespace ballistica::base
|