diff --git a/.efrocachemap b/.efrocachemap index fb411c5e..0ff4b268 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3934,14 +3934,14 @@ "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ac/96/c3b9934061393fe09cc90ff24b8d", "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/38/2b/5641b3b40846f74f232771ac0457", - "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e3/43/5df3f99b46aa9b6aff2db87fabb4", - "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/b1/06/b993362a65a5760ddfb10f2c59b9", - "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4f/99/2234c89a65d7e5d8463556d9df5e", - "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e4/96/f289ec31cdada6f41de5dce382bd", - "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/62/8d/6f2752d858b097c318920ff09cf5", - "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/77/1d/7986d62c92d9f34ca85396168923", - "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/a3/da/d5f88e92926543f61bd5dd80321e", - "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/60/8e/d7efd5e7d7a94439453909527d4e", - "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d3/b3/8124ea626db8dbd7b1744c335c7c", - "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/27/f7/87a5d3b8648352ac40398f86dc8e" + "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d0/21/cb738075207c3dab9f2389455a69", + "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/cd/44/d59a1f7d6ee05b58e80ebc3469bf", + "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b4/56/6d1f390d348ddf65937af3374d76", + "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/31/b9/176ba76e48c23d064b6583963a0e", + "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/37/18/56c6d3b8788362ceb97005b6b3b6", + "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a6/bd/da7954fe559403d021b15df72967", + "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/97/10/138625f676793653620a84cd712b", + "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/ef/a8/4b532f37f74929b834ffd6c2ac5d", + "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/3f/6f/c49674367960a82b19a45641daa6", + "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/df/a3/90e19f2ef6b2edd17d0f936245f2" } \ No newline at end of file diff --git a/src/ballistica/app/app.cc b/src/ballistica/app/app.cc new file mode 100644 index 00000000..ad1b686f --- /dev/null +++ b/src/ballistica/app/app.cc @@ -0,0 +1,483 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/app/app.h" + +#include "ballistica/core/thread.h" +#include "ballistica/game/game.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/input/device/touch_input.h" +#include "ballistica/input/input.h" +#include "ballistica/networking/network_reader.h" +#include "ballistica/networking/networking.h" +#include "ballistica/networking/telnet_server.h" +#include "ballistica/python/python.h" + +namespace ballistica { + +App::App(Thread* thread) : Module("app", thread) { + assert(g_app == nullptr); + g_app = this; + + // So anyone who needs to use the 'main' thread id can get at it... + Thread::UpdateMainThreadID(); + + // We modify some app behavior when run under the server manager. + auto* envval = getenv("BA_SERVER_WRAPPER_MANAGED"); + server_wrapper_managed_ = (envval && strcmp(envval, "1") == 0); +} + +void App::PostInit() { + // If we've got a nice themed hardware cursor, show it. + // Otherwise hide the hardware cursor; we'll draw it in software. + // (need to run this in postinit because SDL/etc may not be inited yet + // as of App::App(). + g_platform->SetHardwareCursorVisible(g_buildconfig.hardware_cursor()); +} + +App::~App() = default; + +auto App::UsesEventLoop() const -> bool { + // We have 2 redundant values for essentially the same thing; + // should get rid of IsEventPushMode() once we've created + // App subclasses for our various platforms. + return !g_platform->IsEventPushMode(); +} + +void App::PushInterruptSignalSetupCall() { + g_platform->SetupInterruptHandling(); +} + +void App::RunRenderUpkeepCycle() { + // This should only be used in cases where the OS is handling the event loop. + assert(!UsesEventLoop()); + if (UsesEventLoop()) { + return; + } + + // Pump thread messages (we're being driven by frame-draw callbacks + // so this is the only place that it gets done at). + thread()->RunEventLoop(true); // Single pass only. + + // Now do the general app event cycle for whoever needs to process things. + RunEvents(); +} + +void App::RebuildLostGLContext() { + assert(InMainThread()); + assert(g_graphics_server); + if (g_graphics_server) { + g_graphics_server->RebuildLostContext(); + } +} + +void App::DrawFrame(bool during_resize) { + assert(InMainThread()); + + // Its possible to receive frames before we're ready to draw. + if (!g_graphics_server || !g_graphics_server->renderer()) { + return; + } + + millisecs_t starttime = GetRealTime(); + + // A resize-draw event means that we're drawing due to a window resize. + // In this case we ignore regular draw events for a short while + // afterwards which makes resizing smoother. + // FIXME: should figure out the *correct* way to handle this; + // I believe the underlying cause here is some sort of context contention + // across threads. + if (during_resize) { + last_resize_draw_event_time_ = starttime; + } else { + if (starttime - last_resize_draw_event_time_ < (1000 / 30)) { + return; + } + } + g_graphics_server->TryRender(); + RunRenderUpkeepCycle(); +} + +void App::SetScreenResolution(float width, float height) { + assert(InMainThread()); + if (!HeadlessMode()) { + g_graphics_server->VideoResize(width, height); + } +} + +void App::PushShutdownCompleteCall() { + PushCall([this] { ShutdownComplete(); }); +} + +void App::ShutdownComplete() { + assert(g_platform); + + // Need to call our cleanup stuff that would otherwise get called in main. + g_platform->FinalCleanup(); + + done_ = true; + + // Kill our own event loop (or tell the OS to kill its). + if (UsesEventLoop()) { + thread()->Quit(); + } else { + g_platform->QuitApp(); + } +} + +void App::RunEvents() { + if (!HeadlessMode()) { + // there's probably a better place for this... + UpdateStressTesting(); + } + + // Give platforms a chance to pump/handle their own events. + // FIXME: now that we have app class overrides, platform should really + // not be doing event handling. (need to fix rift build). + g_platform->RunEvents(); +} + +void App::UpdatePauseResume() { + if (actually_paused_) { + // Unpause if no one wants pause. + if (!sys_paused_app_ && !user_paused_app_) { + OnResume(); + actually_paused_ = false; + } + } else { + // Pause if anyone wants. + if (sys_paused_app_ || user_paused_app_) { + OnPause(); + actually_paused_ = true; + } + } +} + +void App::OnPause() { + assert(InMainThread()); + + // Avoid reading gyro values for a short time to avoid hitches when restored. + g_graphics->SetGyroEnabled(false); + + // IMPORTANT: Any on-pause related stuff that threads need to do must + // must be done from their HandleThreadPause(). If we push runnables to them + // they may or may not be called before the thread is actually paused. + + Thread::SetThreadsPaused(true); + + assert(g_networking); + g_networking->Pause(); + + assert(g_network_reader); + if (g_network_reader) { + g_network_reader->Pause(); + } + + if (g_app_globals->telnet_server) { + g_app_globals->telnet_server->Pause(); + } + + g_platform->OnAppPause(); +} + +void App::OnResume() { + assert(InMainThread()); + last_app_resume_time_ = GetRealTime(); + Thread::SetThreadsPaused(false); + + g_platform->OnAppResume(); + g_networking->Resume(); + g_network_reader->Resume(); + + if (g_app_globals->telnet_server) { + g_app_globals->telnet_server->Resume(); + } + + // Also let the Python layer do what it needs to + // (starting/stopping music, etc). + g_python->PushObjCall(Python::ObjID::kHandleAppResumeCall); + g_game->PushOnAppResumeCall(); + + g_graphics->SetGyroEnabled(true); + + // When resuming from a paused state, we may want to + // pause whatever game was running when we last were active. + // TODO(efro): we should make this smarter so it doesn't happen if + // we're in a network game or something that we can't pause; + // bringing up the menu doesn't really accomplish anything there. + if (g_app_globals->should_pause) { + g_app_globals->should_pause = false; + + // If we've been completely backgrounded, + // send a menu-press command to the game; this will + // bring up a pause menu if we're in the game/etc. + g_game->PushMainMenuPressCall(nullptr); + } +} + +auto App::GetProductPrice(const std::string& product) -> std::string { + std::lock_guard lock(product_prices_mutex_); + auto i = product_prices_.find(product); + if (i == product_prices_.end()) { + return ""; + } else { + return i->second; + } +} + +void App::SetProductPrice(const std::string& product, + const std::string& price) { + std::lock_guard lock(product_prices_mutex_); + product_prices_[product] = price; +} + +void App::PauseApp() { + assert(InMainThread()); + Platform::DebugLog("PauseApp@" + + std::to_string(Platform::GetCurrentMilliseconds())); + assert(!sys_paused_app_); + sys_paused_app_ = true; + UpdatePauseResume(); +} + +void App::ResumeApp() { + assert(InMainThread()); + Platform::DebugLog("ResumeApp@" + + std::to_string(Platform::GetCurrentMilliseconds())); + assert(sys_paused_app_); + sys_paused_app_ = false; + UpdatePauseResume(); +} + +void App::DidFinishRenderingFrame(FrameDef* frame) {} + +void App::PrimeEventPump() { + assert(!UsesEventLoop()); + + // Pump events manually until a screen gets created. + // At that point we use frame-draws to drive our event loop. + while (!g_graphics_server->initial_screen_created()) { + g_main_thread->RunEventLoop(true); + Platform::SleepMS(1); + } +} + +#pragma mark Push-Calls + +void App::PushShowOnlineScoreUICall(const std::string& show, + const std::string& game, + const std::string& game_version) { + PushCall([this, show, game, game_version] { + assert(InMainThread()); + g_platform->ShowOnlineScoreUI(show, game, game_version); + }); +} + +void App::PushNetworkSetupCall(int port, int telnet_port, bool enable_telnet, + const std::string& telnet_password) { + PushCall([this, port, telnet_port, enable_telnet, telnet_password] { + assert(InMainThread()); + // Kick these off if they don't exist. + // (do we want to support changing ports on existing ones?) + if (g_network_reader == nullptr) { + new NetworkReader(port); + } + if (g_app_globals->telnet_server == nullptr && enable_telnet) { + new TelnetServer(telnet_port); + assert(g_app_globals->telnet_server); + if (telnet_password.empty()) { + g_app_globals->telnet_server->SetPassword(nullptr); + } else { + g_app_globals->telnet_server->SetPassword(telnet_password.c_str()); + } + } + }); +} + +void App::PushPurchaseAckCall(const std::string& purchase, + const std::string& order_id) { + PushCall([this, purchase, order_id] { + g_platform->PurchaseAck(purchase, order_id); + }); +} + +void App::PushGetScoresToBeatCall(const std::string& level, + const std::string& config, + void* py_callback) { + PushCall([this, level, config, py_callback] { + assert(InMainThread()); + g_platform->GetScoresToBeat(level, config, py_callback); + }); +} + +void App::PushPurchaseCall(const std::string& item) { + PushCall([this, item] { + assert(InMainThread()); + g_platform->Purchase(item); + }); +} + +void App::PushRestorePurchasesCall() { + PushCall([this] { + assert(InMainThread()); + g_platform->RestorePurchases(); + }); +} + +void App::PushOpenURLCall(const std::string& url) { + PushCall([this, url] { g_platform->OpenURL(url); }); +} + +void App::PushGetFriendScoresCall(const std::string& game, + const std::string& game_version, void* data) { + PushCall([this, game, game_version, data] { + g_platform->GetFriendScores(game, game_version, data); + }); +} + +void App::PushSubmitScoreCall(const std::string& game, + const std::string& game_version, int64_t score) { + PushCall([this, game, game_version, score] { + g_platform->SubmitScore(game, game_version, score); + }); +} + +void App::PushAchievementReportCall(const std::string& achievement) { + PushCall([this, achievement] { g_platform->ReportAchievement(achievement); }); +} + +void App::PushStringEditCall(const std::string& name, const std::string& value, + int max_chars) { + PushCall([this, name, value, max_chars] { + static millisecs_t last_edit_time = 0; + millisecs_t t = GetRealTime(); + + // Ignore if too close together. + // (in case second request comes in before first takes effect). + if (t - last_edit_time < 1000) { + return; + } + last_edit_time = t; + assert(InMainThread()); + g_platform->EditText(name, value, max_chars); + }); +} + +void App::PushSetStressTestingCall(bool enable, int player_count) { + PushCall([this, enable, player_count] { + bool was_stress_testing = stress_testing_; + stress_testing_ = enable; + stress_test_player_count_ = player_count; + + // If we're turning on, reset our intervals and things. + if (!was_stress_testing && stress_testing_) { + // So our first sample is 1 interval from now... + last_stress_test_update_time_ = GetRealTime(); + // Reset our frames-rendered tally. + if (g_graphics && g_graphics_server->renderer()) { + last_total_frames_rendered_ = + g_graphics_server->renderer()->total_frames_rendered(); + } else { + // Assume zero if there's no graphics yet. + last_total_frames_rendered_ = 0; + } + } + }); +} + +void App::PushResetAchievementsCall() { + PushCall([this] { g_platform->ResetAchievements(); }); +} + +void App::OnBootstrapComplete() { + assert(InMainThread()); + assert(g_input); + + if (!HeadlessMode()) { + // On desktop systems we just assume keyboard input exists and add it + // immediately. + if (g_platform->IsRunningOnDesktop()) { + g_input->PushCreateKeyboardInputDevices(); + } + + // On non-tv, non-desktop, non-vr systems, create a touchscreen input. + if (!g_platform->IsRunningOnTV() && !IsVRMode() + && !g_platform->IsRunningOnDesktop()) { + g_input->CreateTouchInput(); + } + } +} + +void App::PushCursorUpdate(bool vis) { + PushCall([this, vis] { + assert(InMainThread()); + g_platform->SetHardwareCursorVisible(vis); + }); +} + +void App::UpdateStressTesting() { + // Handle a little misc stuff here. + // If we're currently running stress-tests, update that stuff. + if (stress_testing_ && g_input) { + // Update our fake inputs to make our dudes run around. + g_input->ProcessStressTesting(stress_test_player_count_); + + // Every 10 seconds update our stress-test stats. + millisecs_t t = GetRealTime(); + if (t - last_stress_test_update_time_ >= 10000) { + if (stress_test_stats_file_ == nullptr) { + assert(g_platform); + std::string f_name = + g_platform->GetUserPythonDirectory() + "/stress_test_stats.csv"; + stress_test_stats_file_ = g_platform->FOpen(f_name.c_str(), "wb"); + if (stress_test_stats_file_ != nullptr) { + fprintf(stress_test_stats_file_, + "time,averageFps,nodes,models,collideModels,textures,sounds," + "pssMem,sharedDirtyMem,privateDirtyMem\n"); + fflush(stress_test_stats_file_); + if (g_buildconfig.ostype_android()) { + // On android, let the OS know we've added or removed a file + // (limit to android or we'll get an unimplemented warning) + g_platform->AndroidRefreshFile(f_name); + } + } + } + if (stress_test_stats_file_ != nullptr) { + // See how many frames we've rendered this past interval. + int total_frames_rendered; + if (g_graphics && g_graphics_server->renderer()) { + total_frames_rendered = + g_graphics_server->renderer()->total_frames_rendered(); + } else { + total_frames_rendered = last_total_frames_rendered_; + } + float avg = + static_cast(total_frames_rendered + - last_total_frames_rendered_) + / (static_cast(t - last_stress_test_update_time_) / 1000.0f); + last_total_frames_rendered_ = total_frames_rendered; + uint32_t model_count = 0; + uint32_t collide_model_count = 0; + uint32_t texture_count = 0; + uint32_t sound_count = 0; + uint32_t node_count = 0; + if (g_media) { + model_count = g_media->total_model_count(); + collide_model_count = g_media->total_collide_model_count(); + texture_count = g_media->total_texture_count(); + sound_count = g_media->total_sound_count(); + } + assert(g_game); + std::string mem_usage = g_platform->GetMemUsageInfo(); + fprintf(stress_test_stats_file_, "%d,%.1f,%d,%d,%d,%d,%d,%s\n", + static_cast(GetRealTime()), avg, node_count, model_count, + collide_model_count, texture_count, sound_count, + mem_usage.c_str()); + fflush(stress_test_stats_file_); + } + last_stress_test_update_time_ = t; + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/app/app.h b/src/ballistica/app/app.h new file mode 100644 index 00000000..655aad86 --- /dev/null +++ b/src/ballistica/app/app.h @@ -0,0 +1,148 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_APP_APP_H_ +#define BALLISTICA_APP_APP_H_ + +#include +#include +#include + +#include "ballistica/core/module.h" + +namespace ballistica { + +/// Our high level app interface module. +/// It runs in the main thread and is what platform wrappers +/// should primarily interact with. +class App : public Module { + public: + explicit App(Thread* thread); + ~App() override; + + /// This gets run after the constructor completes. + /// Any setup that may trigger a virtual method/etc. should go here. + void PostInit(); + + /// Return whether this class runs its own event loop. + /// If true, BallisticaMain() will continuously ask the app for events + /// until the app is quit, at which point BallisticaMain() returns. + /// If false, BallisticaMain returns immediately and it is assumed + /// that the OS handles the app lifecycle and pushes events to the app + /// via callbacks/etc. + auto UsesEventLoop() const -> bool; + + /// Called for non-event-loop apps to give them an opportunity to + /// ensure they are self-sustaining. For instance, an app relying on + /// frame-draws for its main thread event processing may need to + /// manually pump events until frame rendering begins. + virtual void PrimeEventPump(); + + /// Handle any pending OS events. + /// On normal graphical builds this is triggered by RunRenderUpkeepCycle(); + /// timer intervals for headless builds, etc. + /// Should process any pending OS events, etc. + virtual void RunEvents(); + + // These should be called by the window, view-controller, sdl, + // or whatever is driving the app. They must be called from the main thread. + + /// Should be called on mobile when the app is backgrounded. + /// Pauses threads, closes network sockets, etc. + void PauseApp(); + + auto paused() const -> bool { return actually_paused_; } + + /// Should be called on mobile when the app is foregrounded. + /// Spins threads back up, re-opens network sockets, etc. + void ResumeApp(); + + /// The last time the app was resumed (uses GetRealTime() value). + auto last_app_resume_time() const -> millisecs_t { + return last_app_resume_time_; + } + + /// Should be called when the window/screen resolution changes. + void SetScreenResolution(float width, float height); + + /// Should be called if the platform detects the GL context was lost. + void RebuildLostGLContext(); + + /// Attempt to draw a frame. + void DrawFrame(bool during_resize = false); + + /// Used on platforms where our main thread event processing is driven by + /// frame-draw commands given to us. This should be called after drawing + /// a frame in order to bring game state up to date and process OS events. + void RunRenderUpkeepCycle(); + + /// Called by the graphics-server when drawing completes for a frame. + virtual void DidFinishRenderingFrame(FrameDef* frame); + + /// Return the price of an IAP product as a human-readable string, + /// or an empty string if not found. + /// FIXME: move this to platform. + auto GetProductPrice(const std::string& product) -> std::string; + void SetProductPrice(const std::string& product, const std::string& price); + + auto done() const -> bool { return done_; } + + /// Whether we're running under ballisticacore_server.py + /// (affects some app behavior). + auto server_wrapper_managed() const -> bool { + return server_wrapper_managed_; + } + + virtual void OnBootstrapComplete(); + + // Deferred calls that can be made from other threads. + + void PushCursorUpdate(bool vis); + void PushShowOnlineScoreUICall(const std::string& show, + const std::string& game, + const std::string& game_version); + void PushGetFriendScoresCall(const std::string& game, + const std::string& game_version, void* data); + void PushSubmitScoreCall(const std::string& game, + const std::string& game_version, int64_t score); + void PushAchievementReportCall(const std::string& achievement); + void PushGetScoresToBeatCall(const std::string& level, + const std::string& config, void* py_callback); + void PushOpenURLCall(const std::string& url); + void PushStringEditCall(const std::string& name, const std::string& value, + int max_chars); + void PushSetStressTestingCall(bool enable, int player_count); + void PushPurchaseCall(const std::string& item); + void PushRestorePurchasesCall(); + void PushResetAchievementsCall(); + void PushPurchaseAckCall(const std::string& purchase, + const std::string& order_id); + void PushNetworkSetupCall(int port, int telnet_port, bool enable_telnet, + const std::string& telnet_password); + void PushShutdownCompleteCall(); + void PushInterruptSignalSetupCall(); + + private: + void UpdateStressTesting(); + void UpdatePauseResume(); + void OnPause(); + void OnResume(); + void ShutdownComplete(); + bool done_{}; + bool server_wrapper_managed_{}; + bool sys_paused_app_{}; + bool user_paused_app_{}; + bool actually_paused_{}; + millisecs_t last_resize_draw_event_time_{}; + millisecs_t last_app_resume_time_{}; + std::map product_prices_; + std::mutex product_prices_mutex_; + FILE* stress_test_stats_file_{}; + millisecs_t last_stress_test_update_time_{}; + int last_total_frames_rendered_{}; + bool stress_testing_{}; + int stress_test_player_count_{8}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_APP_APP_H_ diff --git a/src/ballistica/app/app_config.cc b/src/ballistica/app/app_config.cc new file mode 100644 index 00000000..5a9bbcce --- /dev/null +++ b/src/ballistica/app/app_config.cc @@ -0,0 +1,245 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/app/app_config.h" + +#include + +#include "ballistica/ballistica.h" +#include "ballistica/platform/platform.h" +#include "ballistica/python/python.h" + +namespace ballistica { + +void AppConfig::Init() { new AppConfig(); } + +auto AppConfig::Entry::FloatValue() const -> float { + throw Exception("not a float entry"); +} + +auto AppConfig::Entry::StringValue() const -> std::string { + throw Exception("not a string entry"); +} + +auto AppConfig::Entry::IntValue() const -> int { + throw Exception("not an int entry"); +} + +auto AppConfig::Entry::BoolValue() const -> bool { + throw Exception("not a bool entry"); +} + +auto AppConfig::Entry::DefaultFloatValue() const -> float { + throw Exception("not a float entry"); +} + +auto AppConfig::Entry::DefaultStringValue() const -> std::string { + throw Exception("not a string entry"); +} + +auto AppConfig::Entry::DefaultIntValue() const -> int { + throw Exception("not an int entry"); +} + +auto AppConfig::Entry::DefaultBoolValue() const -> bool { + throw Exception("not a bool entry"); +} + +class AppConfig::StringEntry : public AppConfig::Entry { + public: + StringEntry() = default; + StringEntry(const char* name, std::string default_value) + : Entry(name), default_value_(std::move(default_value)) {} + auto GetType() const -> Type override { return Type::kString; } + auto Resolve() const -> std::string { + return g_python->GetRawConfigValue(name().c_str(), default_value_.c_str()); + } + auto StringValue() const -> std::string override { return Resolve(); } + auto DefaultStringValue() const -> std::string override { + return default_value_; + } + + private: + std::string default_value_; +}; + +class AppConfig::FloatEntry : public AppConfig::Entry { + public: + FloatEntry() = default; + FloatEntry(const char* name, float default_value) + : Entry(name), default_value_(default_value) {} + auto GetType() const -> Type override { return Type::kFloat; } + auto Resolve() const -> float { + return g_python->GetRawConfigValue(name().c_str(), default_value_); + } + auto FloatValue() const -> float override { return Resolve(); } + auto DefaultFloatValue() const -> float override { return default_value_; } + + private: + float default_value_{}; +}; + +class AppConfig::IntEntry : public AppConfig::Entry { + public: + IntEntry() = default; + IntEntry(const char* name, int default_value) + : Entry(name), default_value_(default_value) {} + auto GetType() const -> Type override { return Type::kInt; } + auto Resolve() const -> int { + return g_python->GetRawConfigValue(name().c_str(), default_value_); + } + auto IntValue() const -> int override { return Resolve(); } + auto DefaultIntValue() const -> int override { return default_value_; } + + private: + int default_value_{}; +}; + +class AppConfig::BoolEntry : public AppConfig::Entry { + public: + BoolEntry() = default; + BoolEntry(const char* name, bool default_value) + : Entry(name), default_value_(default_value) {} + auto GetType() const -> Type override { return Type::kBool; } + auto Resolve() const -> bool { + return g_python->GetRawConfigValue(name().c_str(), default_value_); + } + auto BoolValue() const -> bool override { return Resolve(); } + auto DefaultBoolValue() const -> bool override { return default_value_; } + + private: + bool default_value_{}; +}; + +AppConfig::AppConfig() { + // (We're a singleton). + assert(g_app_config == nullptr); + g_app_config = this; + SetupEntries(); +} + +template +void AppConfig::CompleteMap(const T& entry_map) { + for (auto&& i : entry_map) { + assert(entries_by_name_.find(i.second.name()) == entries_by_name_.end()); + assert(i.first < decltype(i.first)::kLast); + entries_by_name_[i.second.name()] = &i.second; + } + + // Make sure all values have entries. +#if BA_DEBUG_BUILD + int last = static_cast(decltype(entry_map.begin()->first)::kLast); // ew + for (int j = 0; j < last; ++j) { + auto i2 = + entry_map.find(static_castfirst)>(j)); + if (i2 == entry_map.end()) { + throw Exception("Missing appconfig entry " + std::to_string(j)); + } + } +#endif +} + +void AppConfig::SetupEntries() { + // Register all our typed entries. + float_entries_[FloatID::kScreenGamma] = FloatEntry("Screen Gamma", 1.0F); + float_entries_[FloatID::kScreenPixelScale] = + FloatEntry("Screen Pixel Scale", 1.0F); + float_entries_[FloatID::kTouchControlsScale] = + FloatEntry("Touch Controls Scale", 1.0F); + float_entries_[FloatID::kTouchControlsScaleMovement] = + FloatEntry("Touch Controls Scale Movement", 1.0F); + float_entries_[FloatID::kTouchControlsScaleActions] = + FloatEntry("Touch Controls Scale Actions", 1.0F); + float_entries_[FloatID::kSoundVolume] = FloatEntry("Sound Volume", 1.0F); + float_entries_[FloatID::kMusicVolume] = FloatEntry("Music Volume", 1.0F); + + // Note: keep this synced with the defaults in MainActivity.java. + float gvrrts_default = g_platform->IsRunningOnDaydream() ? 1.0F : 0.5F; + float_entries_[FloatID::kGoogleVRRenderTargetScale] = + FloatEntry("GVR Render Target Scale", gvrrts_default); + + string_entries_[StringID::kResolutionAndroid] = + StringEntry("Resolution (Android)", "Auto"); + string_entries_[StringID::kTouchActionControlType] = + StringEntry("Touch Action Control Type", "buttons"); + string_entries_[StringID::kTouchMovementControlType] = + StringEntry("Touch Movement Control Type", "swipe"); + string_entries_[StringID::kGraphicsQuality] = + StringEntry("Graphics Quality", "Auto"); + string_entries_[StringID::kTextureQuality] = + StringEntry("Texture Quality", "Auto"); + string_entries_[StringID::kVerticalSync] = + StringEntry("Vertical Sync", "Auto"); + string_entries_[StringID::kVRHeadRelativeAudio] = + StringEntry("VR Head Relative Audio", "Auto"); + string_entries_[StringID::kMacControllerSubsystem] = + StringEntry("Mac Controller Subsystem", "Classic"); + string_entries_[StringID::kTelnetPassword] = + StringEntry("Telnet Password", "changeme"); + + int_entries_[IntID::kPort] = IntEntry("Port", kDefaultPort); + int_entries_[IntID::kTelnetPort] = + IntEntry("Telnet Port", kDefaultTelnetPort); + + bool_entries_[BoolID::kTouchControlsSwipeHidden] = + BoolEntry("Touch Controls Swipe Hidden", false); + bool_entries_[BoolID::kFullscreen] = BoolEntry("Fullscreen", false); + bool_entries_[BoolID::kKickIdlePlayers] = + BoolEntry("Kick Idle Players", false); + bool_entries_[BoolID::kAlwaysUseInternalKeyboard] = + BoolEntry("Always Use Internal Keyboard", false); + bool_entries_[BoolID::kShowFPS] = BoolEntry("Show FPS", false); + bool_entries_[BoolID::kTVBorder] = + BoolEntry("TV Border", g_platform->IsRunningOnTV()); + bool_entries_[BoolID::kKeyboardP2Enabled] = + BoolEntry("Keyboard P2 Enabled", false); + bool_entries_[BoolID::kEnablePackageMods] = + BoolEntry("Enable Package Mods", false); + bool_entries_[BoolID::kChatMuted] = BoolEntry("Chat Muted", false); + bool_entries_[BoolID::kEnableRemoteApp] = + BoolEntry("Enable Remote App", true); + bool_entries_[BoolID::kEnableTelnet] = BoolEntry("Enable Telnet", true); + bool_entries_[BoolID::kDisableCameraShake] = + BoolEntry("Disable Camera Shake", false); + bool_entries_[BoolID::kDisableCameraGyro] = + BoolEntry("Disable Camera Gyro", false); + + // Now add everything to our name map and make sure all is kosher. + CompleteMap(float_entries_); + CompleteMap(int_entries_); + CompleteMap(string_entries_); + CompleteMap(bool_entries_); +} + +auto AppConfig::Resolve(FloatID id) -> float { + auto i = float_entries_.find(id); + if (i == float_entries_.end()) { + throw Exception("Invalid config entry"); + } + return i->second.Resolve(); +} + +auto AppConfig::Resolve(StringID id) -> std::string { + auto i = string_entries_.find(id); + if (i == string_entries_.end()) { + throw Exception("Invalid config entry"); + } + return i->second.Resolve(); +} + +auto AppConfig::Resolve(BoolID id) -> bool { + auto i = bool_entries_.find(id); + if (i == bool_entries_.end()) { + throw Exception("Invalid config entry"); + } + return i->second.Resolve(); +} + +auto AppConfig::Resolve(IntID id) -> int { + auto i = int_entries_.find(id); + if (i == int_entries_.end()) { + throw Exception("Invalid config entry"); + } + return i->second.Resolve(); +} + +} // namespace ballistica diff --git a/src/ballistica/app/app_config.h b/src/ballistica/app/app_config.h new file mode 100644 index 00000000..80d4d3f9 --- /dev/null +++ b/src/ballistica/app/app_config.h @@ -0,0 +1,130 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_APP_APP_CONFIG_H_ +#define BALLISTICA_APP_APP_CONFIG_H_ + +#include +#include +#include +#include + +namespace ballistica { + +// This class wrangles user config values for the app. +// The underlying config data currently lives in the Python layer, +// so at the moment these calls are only usable from the game thread, +// but that may change in the future. +class AppConfig { + public: + // Our official config values: + + enum class FloatID { + kScreenGamma, + kScreenPixelScale, + kTouchControlsScale, + kTouchControlsScaleMovement, + kTouchControlsScaleActions, + kSoundVolume, + kMusicVolume, + kGoogleVRRenderTargetScale, + kLast // Sentinel. + }; + + enum class StringID { + kResolutionAndroid, + kTouchActionControlType, + kTouchMovementControlType, + kGraphicsQuality, + kTextureQuality, + kVerticalSync, + kVRHeadRelativeAudio, + kMacControllerSubsystem, + kTelnetPassword, + kLast // Sentinel. + }; + + enum class IntID { + kPort, + kTelnetPort, + kLast // Sentinel. + }; + + enum class BoolID { + kTouchControlsSwipeHidden, + kFullscreen, + kKickIdlePlayers, + kAlwaysUseInternalKeyboard, + kShowFPS, + kTVBorder, + kKeyboardP2Enabled, + kEnablePackageMods, + kChatMuted, + kEnableRemoteApp, + kEnableTelnet, + kDisableCameraShake, + kDisableCameraGyro, + kLast // Sentinel. + }; + + class Entry { + public: + enum class Type { kString, kInt, kFloat, kBool }; + Entry() = default; + explicit Entry(const char* name) : name_(name) {} + virtual auto GetType() const -> Type = 0; + auto name() const -> const std::string& { return name_; } + virtual auto FloatValue() const -> float; + virtual auto StringValue() const -> std::string; + virtual auto IntValue() const -> int; + virtual auto BoolValue() const -> bool; + virtual auto DefaultFloatValue() const -> float; + virtual auto DefaultStringValue() const -> std::string; + virtual auto DefaultIntValue() const -> int; + virtual auto DefaultBoolValue() const -> bool; + + private: + std::string name_; + }; + + static void Init(); + AppConfig(); + + // Given specific ids, returns resolved values (fastest access). + auto Resolve(FloatID id) -> float; + auto Resolve(StringID id) -> std::string; + auto Resolve(IntID id) -> int; + auto Resolve(BoolID id) -> bool; + + // Given a name, returns an entry (or nullptr). + // You should check the entry's type and request + // the corresponding typed resolved value from it. + auto GetEntry(const std::string& name) -> const Entry* { + auto i = entries_by_name_.find(name); + if (i == entries_by_name_.end()) { + return nullptr; + } + return i->second; + } + + auto entries_by_name() const -> const std::map& { + return entries_by_name_; + } + + private: + class StringEntry; + class FloatEntry; + class IntEntry; + class BoolEntry; + template + void CompleteMap(const T& entry_map); + void SetupEntries(); + std::map entries_by_name_; + std::map float_entries_; + std::map int_entries_; + std::map string_entries_; + std::map bool_entries_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_APP_APP_CONFIG_H_ diff --git a/src/ballistica/app/app_globals.cc b/src/ballistica/app/app_globals.cc new file mode 100644 index 00000000..3afbb3fe --- /dev/null +++ b/src/ballistica/app/app_globals.cc @@ -0,0 +1,12 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/app/app_globals.h" + +namespace ballistica { + +AppGlobals::AppGlobals(int argc_in, char** argv_in) + : argc{argc_in}, + argv{argv_in}, + main_thread_id{std::this_thread::get_id()} {} + +} // namespace ballistica diff --git a/src/ballistica/app/app_globals.h b/src/ballistica/app/app_globals.h new file mode 100644 index 00000000..7af2803e --- /dev/null +++ b/src/ballistica/app/app_globals.h @@ -0,0 +1,99 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_APP_APP_GLOBALS_H_ +#define BALLISTICA_APP_APP_GLOBALS_H_ + +#include +#include +#include +#include +#include + +#include "ballistica/ballistica.h" +#include "ballistica/networking/master_server_config.h" + +namespace ballistica { + +// The first thing the engine does is allocate an instance of this as g_globals. +// As much as possible, previously static/global values should be moved to here, +// ideally as a temporary measure until they can be placed as non-static members +// in the proper classes. +// Any use of non-trivial global/static values such as class instances should be +// avoided since it can introduce ambiguities during init and teardown. +// For more explanation, see the 'Static and Global Variables' section in the +// Google C++ Style Guide. +class AppGlobals { + public: + AppGlobals(int argc, char** argv); + + /// Program argument count (on applicable platforms). + int argc{}; + + /// Program argument values (on applicable platforms). + char** argv{}; + + std::map node_types; + std::map node_types_by_id; + std::map node_message_types; + std::vector node_message_formats; + std::string calced_blessing_hash; + bool have_mods{}; + bool replay_open{}; + std::vector pausable_threads; + TouchInput* touch_input{}; + std::string console_startup_messages; + std::mutex log_mutex; + std::string log; + bool put_log{}; + bool log_full{}; + int master_server_source{1}; + int session_count{}; + bool shutting_down{}; + bool have_incentivized_ad{true}; + bool should_pause{}; + TelnetServer* telnet_server{}; + Console* console{}; + bool reset_vr_orientation{}; + bool user_ran_commands{}; + UIScale ui_scale{UIScale::kLarge}; + AccountType account_type{AccountType::kInvalid}; + bool remote_server_accepting_connections{true}; + std::string game_commands; + std::string user_agent_string{"BA_USER_AGENT_UNSET (" BA_PLATFORM_STRING ")"}; + int return_value{}; + bool is_stdin_a_terminal{true}; + std::thread::id main_thread_id{}; + bool is_bootstrapped{}; + bool args_handled{}; + std::string user_config_dir; + bool started_suicide{}; + + // Netplay testing. + int buffer_time{1000 / 30}; + + // How often we send dynamics sync packets. + int dynamics_sync_time{500}; + + // How many steps we sample for each bucket. + int delay_samples{20}; + + bool vr_mode{g_buildconfig.vr_build()}; + // Temp dirty way to do some shutdown stuff (FIXME: move to an App method). + void (*temp_cleanup_callback)() = nullptr; + millisecs_t real_time{}; + millisecs_t last_real_time_ticks{}; + std::mutex real_time_mutex; + std::mutex thread_name_map_mutex; + std::map thread_name_map; + std::string master_server_addr{BA_MASTER_SERVER_DEFAULT_ADDR}; + std::string master_server_fallback_addr{BA_MASTER_SERVER_FALLBACK_ADDR}; +#if BA_DEBUG_BUILD + std::mutex object_list_mutex; + Object* object_list_first{}; + int object_count{0}; +#endif +}; + +} // namespace ballistica + +#endif // BALLISTICA_APP_APP_GLOBALS_H_ diff --git a/src/ballistica/app/headless_app.cc b/src/ballistica/app/headless_app.cc new file mode 100644 index 00000000..6d88748e --- /dev/null +++ b/src/ballistica/app/headless_app.cc @@ -0,0 +1,21 @@ +// Copyright (c) 2011-2020 Eric Froemling +#if BA_HEADLESS_BUILD + +#include "ballistica/app/headless_app.h" + +#include "ballistica/ballistica.h" + +namespace ballistica { + +// We could technically use the vanilla App class here since we're not +// changing anything. +HeadlessApp::HeadlessApp(Thread* thread) : App(thread) { + // NewThreadTimer(10, true, NewLambdaRunnable([this] { + // assert(g_app); + // g_app->RunEvents(); + // })); +} + +} // namespace ballistica + +#endif // BA_HEADLESS_BUILD diff --git a/src/ballistica/app/headless_app.h b/src/ballistica/app/headless_app.h new file mode 100644 index 00000000..660b7213 --- /dev/null +++ b/src/ballistica/app/headless_app.h @@ -0,0 +1,20 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_APP_HEADLESS_APP_H_ +#define BALLISTICA_APP_HEADLESS_APP_H_ +#if BA_HEADLESS_BUILD + +#include "ballistica/app/app.h" +#include "ballistica/core/thread.h" + +namespace ballistica { + +class HeadlessApp : public App { + public: + explicit HeadlessApp(Thread* thread); +}; + +} // namespace ballistica + +#endif // BA_HEADLESS_BUILD +#endif // BALLISTICA_APP_HEADLESS_APP_H_ diff --git a/src/ballistica/app/stress_test.cc b/src/ballistica/app/stress_test.cc new file mode 100644 index 00000000..4846e9ac --- /dev/null +++ b/src/ballistica/app/stress_test.cc @@ -0,0 +1,101 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/app/stress_test.h" + +#include "ballistica/ballistica.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/input/input.h" +#include "ballistica/platform/platform.h" + +namespace ballistica { + +void StressTest::Update() { + assert(InMainThread()); + + // Handle a little misc stuff here. + // If we're currently running stress-tests, update that stuff. + if (stress_testing_ && g_input) { + // Update our fake inputs to make our dudes run around. + g_input->ProcessStressTesting(stress_test_player_count_); + + // Every 10 seconds update our stress-test stats. + millisecs_t t = GetRealTime(); + if (t - last_stress_test_update_time_ >= 10000) { + if (stress_test_stats_file_ == nullptr) { + assert(g_platform); + std::string f_name = + g_platform->GetUserPythonDirectory() + "/stress_test_stats.csv"; + stress_test_stats_file_ = g_platform->FOpen(f_name.c_str(), "wb"); + if (stress_test_stats_file_ != nullptr) { + fprintf(stress_test_stats_file_, + "time,averageFps,nodes,models,collide_models,textures,sounds," + "pssMem,sharedDirtyMem,privateDirtyMem\n"); + fflush(stress_test_stats_file_); + if (g_buildconfig.ostype_android()) { + // On android, let the OS know we've added or removed a file + // (limit to android or we'll get an unimplemented warning). + g_platform->AndroidRefreshFile(f_name); + } + } + } + if (stress_test_stats_file_ != nullptr) { + // See how many frames we've rendered this past interval. + int total_frames_rendered; + if (g_graphics_server && g_graphics_server->renderer()) { + total_frames_rendered = + g_graphics_server->renderer()->total_frames_rendered(); + } else { + total_frames_rendered = last_total_frames_rendered_; + } + float avg = + static_cast(total_frames_rendered + - last_total_frames_rendered_) + / (static_cast(t - last_stress_test_update_time_) / 1000.0f); + last_total_frames_rendered_ = total_frames_rendered; + uint32_t model_count = 0; + uint32_t collide_model_count = 0; + uint32_t texture_count = 0; + uint32_t sound_count = 0; + uint32_t node_count = 0; + if (g_media) { + model_count = g_media->total_model_count(); + collide_model_count = g_media->total_collide_model_count(); + texture_count = g_media->total_texture_count(); + sound_count = g_media->total_sound_count(); + } + assert(g_game); + std::string mem_usage = g_platform->GetMemUsageInfo(); + fprintf(stress_test_stats_file_, "%d,%.1f,%d,%d,%d,%d,%d,%s\n", + static_cast_check_fit(GetRealTime()), avg, node_count, + model_count, collide_model_count, texture_count, sound_count, + mem_usage.c_str()); + fflush(stress_test_stats_file_); + } + last_stress_test_update_time_ = t; + } + } +} + +void StressTest::Set(bool enable, int player_count) { + assert(InMainThread()); + bool was_stress_testing = stress_testing_; + stress_testing_ = enable; + stress_test_player_count_ = player_count; + + // If we're turning on, reset our intervals and things. + if (!was_stress_testing && stress_testing_) { + // So our first sample is 1 interval from now. + last_stress_test_update_time_ = GetRealTime(); + + // Reset our frames-rendered tally. + if (g_graphics_server && g_graphics_server->renderer()) { + last_total_frames_rendered_ = + g_graphics_server->renderer()->total_frames_rendered(); + } else { + // Assume zero if there's no graphics yet. + last_total_frames_rendered_ = 0; + } + } +} +} // namespace ballistica diff --git a/src/ballistica/app/stress_test.h b/src/ballistica/app/stress_test.h new file mode 100644 index 00000000..89c8bed3 --- /dev/null +++ b/src/ballistica/app/stress_test.h @@ -0,0 +1,29 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_APP_STRESS_TEST_H_ +#define BALLISTICA_APP_STRESS_TEST_H_ + +#include "ballistica/ballistica.h" + +namespace ballistica { + +// FIXME: This is not wired up; I just moved things here from App. +class StressTest { + public: + // This used to be a SetStressTesting() call in App. + void Set(bool enable, int player_count); + + // This used to get run from RunEvents() in App. + void Update(); + + private: + FILE* stress_test_stats_file_{}; + millisecs_t last_stress_test_update_time_{}; + bool stress_testing_{}; + int stress_test_player_count_{8}; + int last_total_frames_rendered_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_APP_STRESS_TEST_H_ diff --git a/src/ballistica/app/vr_app.cc b/src/ballistica/app/vr_app.cc new file mode 100644 index 00000000..e7d8909e --- /dev/null +++ b/src/ballistica/app/vr_app.cc @@ -0,0 +1,110 @@ +// Copyright (c) 2011-2020 Eric Froemling +#if BA_VR_BUILD + +#include "ballistica/app/vr_app.h" + +#include "ballistica/game/game.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/renderer.h" + +namespace ballistica { + +VRApp::VRApp(Thread* thread) : App(thread) {} + +void VRApp::PushVRSimpleRemoteStateCall(const VRSimpleRemoteState& state) { + PushCall([this, state] { + // Convert this to a full hands state, adding in some simple elbow + // positioning of our own and left/right. + VRHandsState s; + s.l.tx = -0.2f; + s.l.ty = -0.2f; + s.l.tz = -0.3f; + + // Hmm; for now lets always assign this as right hand even when its in + // left-handed mode to keep things simple on the back-end. Can change later + // if there's a downside to that. + s.r.type = VRHandType::kDaydreamRemote; + s.r.tx = 0.2f; + s.r.ty = -0.2f; + s.r.tz = -0.3f; + s.r.yaw = state.r0; + s.r.pitch = state.r1; + s.r.roll = state.r2; + VRSetHands(s); + }); +} + +void VRApp::VRSetDrawDimensions(int w, int h) { + g_graphics_server->VideoResize(w, h); +} + +void VRApp::VRPreDraw() { + if (!g_graphics_server || !g_graphics_server->renderer()) { + return; + } + assert(InMainThread()); + if (FrameDef* frame_def = g_graphics_server->GetRenderFrameDef()) { + // Note: this could be part of PreprocessRenderFrameDef but + // the non-vr path needs it to be separate since preprocess doesn't + // happen sometimes. Should probably clean that up. + g_graphics_server->RunFrameDefMeshUpdates(frame_def); + + // store this for the duration of this frame + vr_render_frame_def_ = frame_def; + g_graphics_server->PreprocessRenderFrameDef(frame_def); + } +} + +void VRApp::VRPostDraw() { + assert(InMainThread()); + if (!g_graphics_server || !g_graphics_server->renderer()) { + return; + } + if (vr_render_frame_def_) { + g_graphics_server->FinishRenderFrameDef(vr_render_frame_def_); + vr_render_frame_def_ = nullptr; + } + RunRenderUpkeepCycle(); +} + +void VRApp::VRSetHead(float tx, float ty, float tz, float yaw, float pitch, + float roll) { + assert(InMainThread()); + Renderer* renderer = g_graphics_server->renderer(); + if (renderer == nullptr) return; + renderer->VRSetHead(tx, ty, tz, yaw, pitch, roll); +} + +void VRApp::VRSetHands(const VRHandsState& state) { + assert(InMainThread()); + + // Pass this along to the renderer (in this same thread) for drawing + // (so hands can be drawn at their absolute most up-to-date positions, etc). + Renderer* renderer = g_graphics_server->renderer(); + if (renderer == nullptr) return; + renderer->VRSetHands(state); + + // ALSO ship it off to the game/ui thread to actually handle input from it. + g_game->PushVRHandsState(state); +} + +void VRApp::VRDrawEye(int eye, float yaw, float pitch, float roll, float tan_l, + float tan_r, float tan_b, float tan_t, float eye_x, + float eye_y, float eye_z, int viewport_x, + int viewport_y) { + if (!g_graphics_server || !g_graphics_server->renderer()) { + return; + } + assert(InMainThread()); + if (vr_render_frame_def_) { + // set up VR eye stuff... + Renderer* renderer = g_graphics_server->renderer(); + renderer->VRSetEye(eye, yaw, pitch, roll, tan_l, tan_r, tan_b, tan_t, eye_x, + eye_y, eye_z, viewport_x, viewport_y); + g_graphics_server->DrawRenderFrameDef(vr_render_frame_def_); + } +} + +} // namespace ballistica + +#endif // BA_VR_BUILD diff --git a/src/ballistica/app/vr_app.h b/src/ballistica/app/vr_app.h new file mode 100644 index 00000000..ffa0b694 --- /dev/null +++ b/src/ballistica/app/vr_app.h @@ -0,0 +1,48 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_APP_VR_APP_H_ +#define BALLISTICA_APP_VR_APP_H_ + +#if BA_VR_BUILD + +#include "ballistica/app/app.h" + +namespace ballistica { + +class VRApp : public App { + public: + /// For passing in state of Daydream remote (and maybe gear vr?..). + struct VRSimpleRemoteState { + bool right_handed = true; + float r0 = 0.0f; + float r1 = 0.0f; + float r2 = 0.0f; + }; + + /// Return g_app as a VRApp. (assumes it actually is one). + static VRApp* get() { + assert(g_app != nullptr); + assert(dynamic_cast(g_app) == static_cast(g_app)); + return static_cast(g_app); + } + + explicit VRApp(Thread* thread); + void PushVRSimpleRemoteStateCall(const VRSimpleRemoteState& state); + void VRSetDrawDimensions(int w, int h); + void VRPreDraw(); + void VRPostDraw(); + void VRSetHead(float tx, float ty, float tz, float yaw, float pitch, + float roll); + void VRSetHands(const VRHandsState& state); + void VRDrawEye(int eye, float yaw, float pitch, float roll, float tan_l, + float tan_r, float tan_b, float tan_t, float eye_x, + float eye_y, float eye_z, int viewport_x, int viewport_y); + + private: + FrameDef* vr_render_frame_def_{}; +}; + +} // namespace ballistica + +#endif // BA_VR_BUILD +#endif // BALLISTICA_APP_VR_APP_H_