From 6ce5330f5f240f7ea5bcd06a49794480a339860a Mon Sep 17 00:00:00 2001 From: Triang3l Date: Sat, 28 Aug 2021 19:38:24 +0300 Subject: [PATCH] [UI] Loop thread to main thread WindowedAppContext --- premake5.lua | 12 +- src/xenia/app/emulator_window.cc | 39 +- src/xenia/app/emulator_window.h | 14 +- src/xenia/app/premake5.lua | 2 +- src/xenia/app/xenia_main.cc | 392 +++++++++++------- src/xenia/apu/xaudio2/xaudio2_audio_driver.cc | 143 ++++++- src/xenia/apu/xaudio2/xaudio2_audio_driver.h | 33 ++ src/xenia/base/app_win32.manifest | 14 + src/xenia/base/console.h | 22 + src/xenia/base/{main.h => console_app_main.h} | 42 +- src/xenia/base/console_app_main_android.cc | 10 + ...ain_posix.cc => console_app_main_posix.cc} | 34 +- src/xenia/base/console_app_main_win.cc | 36 ++ src/xenia/base/console_posix.cc | 21 + src/xenia/base/console_win.cc | 60 +++ src/xenia/base/cvar.cc | 2 +- src/xenia/base/logging.cc | 2 +- src/xenia/base/main_init_android.cc | 10 + src/xenia/base/main_init_posix.cc | 6 +- src/xenia/base/main_init_win.cc | 3 +- src/xenia/base/main_win.cc | 136 +----- src/xenia/base/main_win.h | 29 ++ src/xenia/base/premake5.lua | 3 +- src/xenia/cpu/ppc/testing/ppc_testing_main.cc | 6 +- .../ppc/testing/ppc_testing_native_main.cc | 17 +- src/xenia/cpu/ppc/testing/premake5.lua | 4 +- src/xenia/cpu/testing/sandbox_main.cc | 8 +- src/xenia/cpu/testing/util.h | 1 - src/xenia/debug/ui/debug_window.cc | 40 +- src/xenia/debug/ui/debug_window.h | 15 +- src/xenia/emulator.cc | 6 +- src/xenia/gpu/d3d12/d3d12_graphics_system.cc | 2 +- src/xenia/gpu/d3d12/d3d12_trace_dump_main.cc | 8 +- .../gpu/d3d12/d3d12_trace_viewer_main.cc | 29 +- src/xenia/gpu/d3d12/premake5.lua | 4 +- src/xenia/gpu/graphics_system.cc | 33 +- src/xenia/gpu/null/null_graphics_system.cc | 2 +- src/xenia/gpu/premake5.lua | 2 +- src/xenia/gpu/shader_compiler_main.cc | 7 +- src/xenia/gpu/trace_dump.cc | 2 +- src/xenia/gpu/trace_player.cc | 5 +- src/xenia/gpu/trace_player.h | 4 +- src/xenia/gpu/trace_viewer.cc | 80 ++-- src/xenia/gpu/trace_viewer.h | 20 +- src/xenia/gpu/vulkan/premake5.lua | 4 +- .../gpu/vulkan/vulkan_graphics_system.cc | 2 +- .../gpu/vulkan/vulkan_trace_dump_main.cc | 8 +- .../gpu/vulkan/vulkan_trace_viewer_main.cc | 27 +- src/xenia/hid/hid_demo.cc | 208 +++++----- src/xenia/hid/premake5.lua | 2 +- src/xenia/hid/sdl/sdl_input_driver.cc | 18 +- src/xenia/kernel/xam/xam_nui.cc | 20 +- src/xenia/kernel/xam/xam_ui.cc | 29 +- src/xenia/kernel/xthread.cc | 18 - .../tools/api-scanner/api_scanner_loader.h | 5 +- .../tools/api-scanner/api_scanner_main.cc | 18 +- src/xenia/ui/d3d12/d3d12_provider.cc | 7 +- src/xenia/ui/d3d12/d3d12_provider.h | 4 +- src/xenia/ui/d3d12/d3d12_window_demo.cc | 32 +- src/xenia/ui/d3d12/premake5.lua | 2 +- src/xenia/ui/file_picker_win.cc | 34 +- src/xenia/ui/graphics_provider.h | 7 +- src/xenia/ui/loop.cc | 37 -- src/xenia/ui/loop.h | 45 -- src/xenia/ui/loop_gtk.cc | 81 ---- src/xenia/ui/loop_win.cc | 131 ------ src/xenia/ui/loop_win.h | 74 ---- src/xenia/ui/premake5.lua | 1 + src/xenia/ui/vulkan/premake5.lua | 2 +- src/xenia/ui/vulkan/vulkan_provider.cc | 7 +- src/xenia/ui/vulkan/vulkan_provider.h | 4 +- src/xenia/ui/vulkan/vulkan_window_demo.cc | 32 +- src/xenia/ui/window.cc | 4 +- src/xenia/ui/window.h | 21 +- src/xenia/ui/window_demo.cc | 91 ++-- src/xenia/ui/window_demo.h | 44 ++ src/xenia/ui/window_gtk.cc | 14 +- src/xenia/ui/window_gtk.h | 3 +- src/xenia/ui/window_win.cc | 59 +-- src/xenia/ui/window_win.h | 3 +- src/xenia/ui/windowed_app.h | 129 ++++++ src/xenia/ui/windowed_app_context.cc | 180 ++++++++ src/xenia/ui/windowed_app_context.h | 184 ++++++++ src/xenia/ui/windowed_app_context_android.cc | 67 +++ src/xenia/ui/windowed_app_context_android.h | 61 +++ src/xenia/ui/windowed_app_context_gtk.cc | 112 +++++ src/xenia/ui/windowed_app_context_gtk.h | 46 ++ src/xenia/ui/windowed_app_context_win.cc | 130 ++++++ src/xenia/ui/windowed_app_context_win.h | 58 +++ src/xenia/ui/windowed_app_main_posix.cc | 64 +++ src/xenia/ui/windowed_app_main_win.cc | 70 ++++ src/xenia/vfs/premake5.lua | 2 +- src/xenia/vfs/vfs_dump.cc | 9 +- tools/build/scripts/test_suite.lua | 2 +- tools/build/src/test_suite_main.cc | 6 +- 95 files changed, 2343 insertions(+), 1235 deletions(-) create mode 100644 src/xenia/base/app_win32.manifest create mode 100644 src/xenia/base/console.h rename src/xenia/base/{main.h => console_app_main.h} (51%) create mode 100644 src/xenia/base/console_app_main_android.cc rename src/xenia/base/{main_posix.cc => console_app_main_posix.cc} (64%) create mode 100644 src/xenia/base/console_app_main_win.cc create mode 100644 src/xenia/base/console_posix.cc create mode 100644 src/xenia/base/console_win.cc create mode 100644 src/xenia/base/main_init_android.cc create mode 100644 src/xenia/base/main_win.h delete mode 100644 src/xenia/ui/loop.cc delete mode 100644 src/xenia/ui/loop.h delete mode 100644 src/xenia/ui/loop_gtk.cc delete mode 100644 src/xenia/ui/loop_win.cc delete mode 100644 src/xenia/ui/loop_win.h create mode 100644 src/xenia/ui/window_demo.h create mode 100644 src/xenia/ui/windowed_app.h create mode 100644 src/xenia/ui/windowed_app_context.cc create mode 100644 src/xenia/ui/windowed_app_context.h create mode 100644 src/xenia/ui/windowed_app_context_android.cc create mode 100644 src/xenia/ui/windowed_app_context_android.h create mode 100644 src/xenia/ui/windowed_app_context_gtk.cc create mode 100644 src/xenia/ui/windowed_app_context_gtk.h create mode 100644 src/xenia/ui/windowed_app_context_win.cc create mode 100644 src/xenia/ui/windowed_app_context_win.h create mode 100644 src/xenia/ui/windowed_app_main_posix.cc create mode 100644 src/xenia/ui/windowed_app_main_win.cc diff --git a/premake5.lua b/premake5.lua index e44f81c71..55a0523a4 100644 --- a/premake5.lua +++ b/premake5.lua @@ -189,6 +189,12 @@ filter("platforms:Windows") "bcrypt", }) +-- Embed the manifest for things like dependencies and DPI awareness. +filter({"platforms:Windows", "kind:ConsoleApp or WindowedApp"}) + files({ + "src/xenia/base/app_win32.manifest" + }) + -- Create scratch/ path if not os.isdir("scratch") then os.mkdir("scratch") @@ -243,9 +249,13 @@ workspace("xenia") include("third_party/SDL2.lua") end - -- Disable treating warnings as fatal errors for all third party projects: + -- Disable treating warnings as fatal errors for all third party projects, as + -- well as other things relevant only to Xenia itself. for _, prj in ipairs(premake.api.scope.current.solution.projects) do project(prj.name) + removefiles({ + "src/xenia/base/app_win32.manifest" + }) removeflags({ "FatalWarnings", }) diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 1b38a8f96..d8e792aa2 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -11,6 +11,7 @@ #include "third_party/fmt/include/fmt/format.h" #include "third_party/imgui/imgui.h" +#include "xenia/base/assert.h" #include "xenia/base/clock.h" #include "xenia/base/cvar.h" #include "xenia/base/debugging.h" @@ -42,10 +43,11 @@ using xe::ui::UIEvent; const std::string kBaseTitle = "xenia"; -EmulatorWindow::EmulatorWindow(Emulator* emulator) +EmulatorWindow::EmulatorWindow(Emulator* emulator, + ui::WindowedAppContext& app_context) : emulator_(emulator), - loop_(ui::Loop::Create()), - window_(ui::Window::Create(loop_.get(), kBaseTitle)) { + app_context_(app_context), + window_(ui::Window::Create(app_context, kBaseTitle)) { base_title_ = kBaseTitle + #ifdef DEBUG #if _NO_DEBUG_HEAP == 1 @@ -63,23 +65,14 @@ EmulatorWindow::EmulatorWindow(Emulator* emulator) ")"; } -EmulatorWindow::~EmulatorWindow() { - loop_->PostSynchronous([this]() { window_.reset(); }); -} - -std::unique_ptr EmulatorWindow::Create(Emulator* emulator) { - std::unique_ptr emulator_window(new EmulatorWindow(emulator)); - - emulator_window->loop()->PostSynchronous([&emulator_window]() { - xe::threading::set_name("Windowing Loop"); - xe::Profiler::ThreadEnter("Windowing Loop"); - - if (!emulator_window->Initialize()) { - xe::FatalError("Failed to initialize main window"); - return; - } - }); - +std::unique_ptr EmulatorWindow::Create( + Emulator* emulator, ui::WindowedAppContext& app_context) { + assert_true(app_context.IsInUIThread()); + std::unique_ptr emulator_window( + new EmulatorWindow(emulator, app_context)); + if (!emulator_window->Initialize()) { + return nullptr; + } return emulator_window; } @@ -91,8 +84,8 @@ bool EmulatorWindow::Initialize() { UpdateTitle(); - window_->on_closed.AddListener([this](UIEvent* e) { loop_->Quit(); }); - loop_->on_quit.AddListener([this](UIEvent* e) { window_.reset(); }); + window_->on_closed.AddListener( + [this](UIEvent* e) { app_context_.QuitFromUIThread(); }); window_->on_file_drop.AddListener( [this](FileDropEvent* e) { FileDrop(e->filename()); }); @@ -313,7 +306,7 @@ void EmulatorWindow::FileOpen() { file_picker->set_multi_selection(false); file_picker->set_title("Select Content Package"); file_picker->set_extensions({ - {"Supported Files", "*.iso;*.xex;*.xcp;*.*"}, + {"Supported Files", "*.iso;*.xex;*.*"}, {"Disc Image (*.iso)", "*.iso"}, {"Xbox Executable (*.xex)", "*.xex"}, //{"Content Package (*.xcp)", "*.xcp" }, diff --git a/src/xenia/app/emulator_window.h b/src/xenia/app/emulator_window.h index 4d29d9a5b..6dfb69080 100644 --- a/src/xenia/app/emulator_window.h +++ b/src/xenia/app/emulator_window.h @@ -13,9 +13,9 @@ #include #include -#include "xenia/ui/loop.h" #include "xenia/ui/menu_item.h" #include "xenia/ui/window.h" +#include "xenia/ui/windowed_app_context.h" #include "xenia/xbox.h" namespace xe { @@ -27,12 +27,11 @@ namespace app { class EmulatorWindow { public: - virtual ~EmulatorWindow(); - - static std::unique_ptr Create(Emulator* emulator); + static std::unique_ptr Create( + Emulator* emulator, ui::WindowedAppContext& app_context); Emulator* emulator() const { return emulator_; } - ui::Loop* loop() const { return loop_.get(); } + ui::WindowedAppContext& app_context() const { return app_context_; } ui::Window* window() const { return window_.get(); } void UpdateTitle(); @@ -40,7 +39,8 @@ class EmulatorWindow { void SetInitializingShaderStorage(bool initializing); private: - explicit EmulatorWindow(Emulator* emulator); + explicit EmulatorWindow(Emulator* emulator, + ui::WindowedAppContext& app_context); bool Initialize(); @@ -60,7 +60,7 @@ class EmulatorWindow { void ShowCommitID(); Emulator* emulator_; - std::unique_ptr loop_; + ui::WindowedAppContext& app_context_; std::unique_ptr window_; std::string base_title_; uint64_t cursor_hide_time_ = 0; diff --git a/src/xenia/app/premake5.lua b/src/xenia/app/premake5.lua index 66f8beaa2..64ebf0db2 100644 --- a/src/xenia/app/premake5.lua +++ b/src/xenia/app/premake5.lua @@ -52,8 +52,8 @@ project("xenia-app") local_platform_files() files({ "xenia_main.cc", - "../base/main_"..platform_suffix..".cc", "../base/main_init_"..platform_suffix..".cc", + "../ui/windowed_app_main_"..platform_suffix..".cc", }) resincludedirs({ diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc index 91c71d822..39a2a72b4 100644 --- a/src/xenia/app/xenia_main.cc +++ b/src/xenia/app/xenia_main.cc @@ -7,18 +7,29 @@ ****************************************************************************** */ +#include +#include +#include +#include +#include +#include +#include + #include "xenia/app/discord/discord_presence.h" #include "xenia/app/emulator_window.h" #include "xenia/base/cvar.h" #include "xenia/base/debugging.h" #include "xenia/base/logging.h" -#include "xenia/base/main.h" +#include "xenia/base/platform.h" #include "xenia/base/profiling.h" #include "xenia/base/threading.h" #include "xenia/config.h" #include "xenia/debug/ui/debug_window.h" #include "xenia/emulator.h" #include "xenia/ui/file_picker.h" +#include "xenia/ui/window.h" +#include "xenia/ui/windowed_app.h" +#include "xenia/ui/windowed_app_context.h" #include "xenia/vfs/devices/host_path_device.h" // Available audio systems: @@ -44,7 +55,6 @@ #endif // XE_PLATFORM_WIN32 #include "third_party/fmt/include/fmt/format.h" -#include "third_party/xbyak/xbyak/xbyak_util.h" DEFINE_string(apu, "any", "Audio system. Use: [any, nop, sdl, xaudio2]", "APU"); DEFINE_string(gpu, "any", "Graphics system. Use: [any, d3d12, vulkan, null]", @@ -91,83 +101,134 @@ DEFINE_bool(discord, true, "Enable Discord rich presence", "General"); namespace xe { namespace app { -template -class Factory { +class EmulatorApp final : public xe::ui::WindowedApp { + public: + static std::unique_ptr Create( + xe::ui::WindowedAppContext& app_context) { + return std::unique_ptr(new EmulatorApp(app_context)); + } + + ~EmulatorApp(); + + bool OnInitialize() override; + + protected: + void OnDestroy() override; + private: - struct Creator { - std::string name; - std::function is_available; - std::function(Args...)> instantiate; + template + class Factory { + private: + struct Creator { + std::string name; + std::function is_available; + std::function(Args...)> instantiate; + }; + + std::vector creators_; + + public: + void Add(const std::string_view name, std::function is_available, + std::function(Args...)> instantiate) { + creators_.push_back({std::string(name), is_available, instantiate}); + } + + void Add(const std::string_view name, + std::function(Args...)> instantiate) { + auto always_available = []() { return true; }; + Add(name, always_available, instantiate); + } + + template + void Add(const std::string_view name) { + Add(name, DT::IsAvailable, [](Args... args) { + return std::make_unique
(std::forward(args)...); + }); + } + + std::unique_ptr Create(const std::string_view name, Args... args) { + if (!name.empty() && name != "any") { + auto it = std::find_if( + creators_.cbegin(), creators_.cend(), + [&name](const auto& f) { return name.compare(f.name) == 0; }); + if (it != creators_.cend() && (*it).is_available()) { + return (*it).instantiate(std::forward(args)...); + } + return nullptr; + } else { + for (const auto& creator : creators_) { + if (!creator.is_available()) continue; + auto instance = creator.instantiate(std::forward(args)...); + if (!instance) continue; + return instance; + } + return nullptr; + } + } + + std::vector> CreateAll(const std::string_view name, + Args... args) { + std::vector> instances; + if (!name.empty() && name != "any") { + auto it = std::find_if( + creators_.cbegin(), creators_.cend(), + [&name](const auto& f) { return name.compare(f.name) == 0; }); + if (it != creators_.cend() && (*it).is_available()) { + auto instance = (*it).instantiate(std::forward(args)...); + if (instance) { + instances.emplace_back(std::move(instance)); + } + } + } else { + for (const auto& creator : creators_) { + if (!creator.is_available()) continue; + auto instance = creator.instantiate(std::forward(args)...); + if (instance) { + instances.emplace_back(std::move(instance)); + } + } + } + return instances; + } }; - std::vector creators_; + explicit EmulatorApp(xe::ui::WindowedAppContext& app_context); - public: - void Add(const std::string_view name, std::function is_available, - std::function(Args...)> instantiate) { - creators_.push_back({std::string(name), is_available, instantiate}); - } + static std::unique_ptr CreateAudioSystem( + cpu::Processor* processor); + static std::unique_ptr CreateGraphicsSystem(); + static std::vector> CreateInputDrivers( + ui::Window* window); - void Add(const std::string_view name, - std::function(Args...)> instantiate) { - auto always_available = []() { return true; }; - Add(name, always_available, instantiate); - } + void EmulatorThread(); + void ShutdownEmulatorThreadFromUIThread(); - template - void Add(const std::string_view name) { - Add(name, DT::IsAvailable, [](Args... args) { - return std::make_unique
(std::forward(args)...); - }); - } + std::unique_ptr emulator_; + std::unique_ptr emulator_window_; - std::unique_ptr Create(const std::string_view name, Args... args) { - if (!name.empty() && name != "any") { - auto it = std::find_if( - creators_.cbegin(), creators_.cend(), - [&name](const auto& f) { return name.compare(f.name) == 0; }); - if (it != creators_.cend() && (*it).is_available()) { - return (*it).instantiate(std::forward(args)...); - } - return nullptr; - } else { - for (const auto& creator : creators_) { - if (!creator.is_available()) continue; - auto instance = creator.instantiate(std::forward(args)...); - if (!instance) continue; - return instance; - } - return nullptr; - } - } + // Created on demand, used by the emulator. + std::unique_ptr debug_window_; - std::vector> CreateAll(const std::string_view name, - Args... args) { - std::vector> instances; - if (!name.empty() && name != "any") { - auto it = std::find_if( - creators_.cbegin(), creators_.cend(), - [&name](const auto& f) { return name.compare(f.name) == 0; }); - if (it != creators_.cend() && (*it).is_available()) { - auto instance = (*it).instantiate(std::forward(args)...); - if (instance) { - instances.emplace_back(std::move(instance)); - } - } - } else { - for (const auto& creator : creators_) { - if (!creator.is_available()) continue; - auto instance = creator.instantiate(std::forward(args)...); - if (instance) { - instances.emplace_back(std::move(instance)); - } - } - } - return instances; - } + // Refreshing the emulator - placed after its dependencies. + std::atomic emulator_thread_quit_requested_; + std::unique_ptr emulator_thread_event_; + std::thread emulator_thread_; }; -std::unique_ptr CreateAudioSystem(cpu::Processor* processor) { +EmulatorApp::EmulatorApp(xe::ui::WindowedAppContext& app_context) + : xe::ui::WindowedApp(app_context, "xenia", "[Path to .iso/.xex]") { + AddPositionalOption("target"); +} + +EmulatorApp::~EmulatorApp() { + // Should be shut down from OnDestroy if OnInitialize has ever been done, but + // for the most safety as a running thread may be destroyed only after + // joining. + ShutdownEmulatorThreadFromUIThread(); +} + +std::unique_ptr EmulatorApp::CreateAudioSystem( + cpu::Processor* processor) { Factory factory; #if XE_PLATFORM_WIN32 factory.Add("xaudio2"); @@ -177,7 +238,7 @@ std::unique_ptr CreateAudioSystem(cpu::Processor* processor) { return factory.Create(cvars::apu, processor); } -std::unique_ptr CreateGraphicsSystem() { +std::unique_ptr EmulatorApp::CreateGraphicsSystem() { Factory factory; #if XE_PLATFORM_WIN32 factory.Add("d3d12"); @@ -187,7 +248,7 @@ std::unique_ptr CreateGraphicsSystem() { return factory.Create(cvars::gpu); } -std::vector> CreateInputDrivers( +std::vector> EmulatorApp::CreateInputDrivers( ui::Window* window) { std::vector> drivers; if (cvars::hid.compare("nop") == 0) { @@ -215,9 +276,9 @@ std::vector> CreateInputDrivers( return drivers; } -int xenia_main(const std::vector& args) { +bool EmulatorApp::OnInitialize() { Profiler::Initialize(); - Profiler::ThreadEnter("main"); + Profiler::ThreadEnter("Main"); // Figure out where internal files and content should go. std::filesystem::path storage_root = cvars::storage_root; @@ -275,20 +336,55 @@ int xenia_main(const std::vector& args) { } // Create the emulator but don't initialize so we can setup the window. - auto emulator = + emulator_ = std::make_unique("", storage_root, content_root, cache_root); // Main emulator display window. - auto emulator_window = EmulatorWindow::Create(emulator.get()); + emulator_window_ = EmulatorWindow::Create(emulator_.get(), app_context()); + if (!emulator_window_) { + XELOGE("Failed to create the main emulator window"); + return false; + } + + // Setup the emulator and run its loop in a separate thread. + emulator_thread_quit_requested_.store(false, std::memory_order_relaxed); + emulator_thread_event_ = xe::threading::Event::CreateAutoResetEvent(false); + emulator_thread_ = std::thread(&EmulatorApp::EmulatorThread, this); + + return true; +} + +void EmulatorApp::OnDestroy() { + ShutdownEmulatorThreadFromUIThread(); + + if (cvars::discord) { + discord::DiscordPresence::Shutdown(); + } + + Profiler::Dump(); + // The profiler needs to shut down before the graphics context. + Profiler::Shutdown(); + + // TODO(DrChat): Remove this code and do a proper exit. + XELOGI("Cheap-skate exit!"); + std::quick_exit(EXIT_SUCCESS); +} + +void EmulatorApp::EmulatorThread() { + assert_not_null(emulator_thread_event_); + + xe::threading::set_name("Emulator"); + Profiler::ThreadEnter("Emulator"); // Setup and initialize all subsystems. If we can't do something // (unsupported system, memory issues, etc) this will fail early. X_STATUS result = - emulator->Setup(emulator_window->window(), CreateAudioSystem, - CreateGraphicsSystem, CreateInputDrivers); + emulator_->Setup(emulator_window_->window(), CreateAudioSystem, + CreateGraphicsSystem, CreateInputDrivers); if (XFAILED(result)) { XELOGE("Failed to setup emulator: {:08X}", result); - return 1; + app_context().RequestDeferredQuit(); + return; } if (cvars::mount_scratch) { @@ -297,10 +393,11 @@ int xenia_main(const std::vector& args) { if (!scratch_device->Initialize()) { XELOGE("Unable to scan scratch path"); } else { - if (!emulator->file_system()->RegisterDevice(std::move(scratch_device))) { + if (!emulator_->file_system()->RegisterDevice( + std::move(scratch_device))) { XELOGE("Unable to register scratch path"); } else { - emulator->file_system()->RegisterSymbolicLink("scratch:", "\\SCRATCH"); + emulator_->file_system()->RegisterSymbolicLink("scratch:", "\\SCRATCH"); } } } @@ -311,10 +408,10 @@ int xenia_main(const std::vector& args) { if (!cache0_device->Initialize()) { XELOGE("Unable to scan cache0 path"); } else { - if (!emulator->file_system()->RegisterDevice(std::move(cache0_device))) { + if (!emulator_->file_system()->RegisterDevice(std::move(cache0_device))) { XELOGE("Unable to register cache0 path"); } else { - emulator->file_system()->RegisterSymbolicLink("cache0:", "\\CACHE0"); + emulator_->file_system()->RegisterSymbolicLink("cache0:", "\\CACHE0"); } } @@ -323,10 +420,10 @@ int xenia_main(const std::vector& args) { if (!cache1_device->Initialize()) { XELOGE("Unable to scan cache1 path"); } else { - if (!emulator->file_system()->RegisterDevice(std::move(cache1_device))) { + if (!emulator_->file_system()->RegisterDevice(std::move(cache1_device))) { XELOGE("Unable to register cache1 path"); } else { - emulator->file_system()->RegisterSymbolicLink("cache1:", "\\CACHE1"); + emulator_->file_system()->RegisterSymbolicLink("cache1:", "\\CACHE1"); } } @@ -339,79 +436,62 @@ int xenia_main(const std::vector& args) { if (!cache_device->Initialize()) { XELOGE("Unable to scan cache path"); } else { - if (!emulator->file_system()->RegisterDevice(std::move(cache_device))) { + if (!emulator_->file_system()->RegisterDevice(std::move(cache_device))) { XELOGE("Unable to register cache path"); } else { - emulator->file_system()->RegisterSymbolicLink("cache:", "\\CACHE"); + emulator_->file_system()->RegisterSymbolicLink("cache:", "\\CACHE"); } } } // Set a debug handler. // This will respond to debugging requests so we can open the debug UI. - std::unique_ptr debug_window; if (cvars::debug) { - emulator->processor()->set_debug_listener_request_handler( - [&](xe::cpu::Processor* processor) { - if (debug_window) { - return debug_window.get(); + emulator_->processor()->set_debug_listener_request_handler( + [this](xe::cpu::Processor* processor) { + if (debug_window_) { + return debug_window_.get(); } - emulator_window->loop()->PostSynchronous([&]() { - debug_window = xe::debug::ui::DebugWindow::Create( - emulator.get(), emulator_window->loop()); - debug_window->window()->on_closed.AddListener( - [&](xe::ui::UIEvent* e) { - emulator->processor()->set_debug_listener(nullptr); - emulator_window->loop()->Post( - [&]() { debug_window.reset(); }); + app_context().CallInUIThreadSynchronous([this]() { + debug_window_ = xe::debug::ui::DebugWindow::Create(emulator_.get(), + app_context()); + debug_window_->window()->on_closed.AddListener( + [this](xe::ui::UIEvent* e) { + emulator_->processor()->set_debug_listener(nullptr); + app_context().CallInUIThread( + [this]() { debug_window_.reset(); }); }); }); - return debug_window.get(); + // If failed to enqueue the UI thread call, this will just be null. + return debug_window_.get(); }); } - auto evt = xe::threading::Event::CreateAutoResetEvent(false); - emulator->on_launch.AddListener([&](auto title_id, const auto& game_title) { + emulator_->on_launch.AddListener([&](auto title_id, const auto& game_title) { if (cvars::discord) { discord::DiscordPresence::PlayingTitle( game_title.empty() ? "Unknown Title" : std::string(game_title)); } - emulator_window->UpdateTitle(); - evt->Set(); + app_context().CallInUIThread([this]() { emulator_window_->UpdateTitle(); }); + emulator_thread_event_->Set(); }); - emulator->on_shader_storage_initialization.AddListener( - [&](bool initializing) { - emulator_window->SetInitializingShaderStorage(initializing); + emulator_->on_shader_storage_initialization.AddListener( + [this](bool initializing) { + app_context().CallInUIThread([this, initializing]() { + emulator_window_->SetInitializingShaderStorage(initializing); + }); }); - emulator->on_terminate.AddListener([&]() { + emulator_->on_terminate.AddListener([]() { if (cvars::discord) { discord::DiscordPresence::NotPlaying(); } }); - emulator_window->window()->on_closing.AddListener([&](ui::UIEvent* e) { - // This needs to shut down before the graphics context. - Profiler::Shutdown(); - }); - - bool exiting = false; - emulator_window->loop()->on_quit.AddListener([&](ui::UIEvent* e) { - exiting = true; - evt->Set(); - - if (cvars::discord) { - discord::DiscordPresence::Shutdown(); - } - - // TODO(DrChat): Remove this code and do a proper exit. - XELOGI("Cheap-skate exit!"); - exit(0); - }); - // Enable the main menu now that the emulator is properly loaded - emulator_window->window()->EnableMainMenu(); + app_context().CallInUIThread( + [this]() { emulator_window_->window()->EnableMainMenu(); }); // Grab path from the flag or unnamed argument. std::filesystem::path path; @@ -420,50 +500,56 @@ int xenia_main(const std::vector& args) { } // Toggles fullscreen - if (cvars::fullscreen) emulator_window->ToggleFullscreen(); + if (cvars::fullscreen) { + app_context().CallInUIThread( + [this]() { emulator_window_->ToggleFullscreen(); }); + } if (!path.empty()) { // Normalize the path and make absolute. auto abs_path = std::filesystem::absolute(path); - result = emulator->LaunchPath(abs_path); + result = emulator_->LaunchPath(abs_path); if (XFAILED(result)) { xe::FatalError(fmt::format("Failed to launch target: {:08X}", result)); - emulator.reset(); - emulator_window.reset(); - return 1; + app_context().RequestDeferredQuit(); + return; } } - // Now, we're going to use the main thread to drive events related to - // emulation. - while (!exiting) { - xe::threading::Wait(evt.get(), false); - + // Now, we're going to use this thread to drive events related to emulation. + while (!emulator_thread_quit_requested_.load(std::memory_order_relaxed)) { + xe::threading::Wait(emulator_thread_event_.get(), false); while (true) { - emulator->WaitUntilExit(); - if (emulator->TitleRequested()) { - emulator->LaunchNextTitle(); + emulator_->WaitUntilExit(); + if (emulator_->TitleRequested()) { + emulator_->LaunchNextTitle(); } else { break; } } } +} - debug_window.reset(); - emulator.reset(); - - if (cvars::discord) { - discord::DiscordPresence::Shutdown(); +void EmulatorApp::ShutdownEmulatorThreadFromUIThread() { + // TODO(Triang3l): Proper shutdown of the emulator (relying on std::quick_exit + // for now) - currently WaitUntilExit loops forever otherwise (plus possibly + // lots of other things not shutting down correctly now). Some parts of the + // code call the regular std::exit, which seems to be calling destructors (at + // least on Linux), so the entire join is currently commented out. +#if 0 + // Same thread as the one created it, to make sure there's zero possibility of + // a race with the creation of the emulator thread. + assert_true(app_context().IsInUIThread()); + emulator_thread_quit_requested_.store(true, std::memory_order_relaxed); + if (!emulator_thread_.joinable()) { + return; } - - Profiler::Dump(); - Profiler::Shutdown(); - emulator_window.reset(); - return 0; + emulator_thread_event_->Set(); + emulator_thread_.join(); +#endif } } // namespace app } // namespace xe -DEFINE_ENTRY_POINT("xenia", xe::app::xenia_main, "[Path to .iso/.xex]", - "target"); +XE_DEFINE_WINDOWED_APP(xenia, xe::app::EmulatorApp::Create); diff --git a/src/xenia/apu/xaudio2/xaudio2_audio_driver.cc b/src/xenia/apu/xaudio2/xaudio2_audio_driver.cc index f393706c9..00ed1a882 100644 --- a/src/xenia/apu/xaudio2/xaudio2_audio_driver.cc +++ b/src/xenia/apu/xaudio2/xaudio2_audio_driver.cc @@ -9,13 +9,14 @@ #include "xenia/apu/xaudio2/xaudio2_audio_driver.h" -// Must be included before xaudio2.h so we get the right windows.h include. -#include "xenia/base/platform_win.h" - #include "xenia/apu/apu_flags.h" #include "xenia/apu/conversion.h" +#include "xenia/apu/xaudio2/xaudio2_api.h" +#include "xenia/base/assert.h" #include "xenia/base/clock.h" #include "xenia/base/logging.h" +#include "xenia/base/platform_win.h" +#include "xenia/base/threading.h" namespace xe { namespace apu { @@ -73,11 +74,6 @@ bool XAudio2AudioDriver::Initialize() { } } - // First CPU (2.8 default). XAUDIO2_ANY_PROCESSOR (2.7 default) steals too - // much time from other things. Ideally should process audio on what roughly - // represents thread 4 (5th) on the Xbox 360 (2.7 default on the console), or - // even beyond the 6 guest cores. - api::XAUDIO2_PROCESSOR processor = 0x00000001; if (api_minor_version_ >= 8) { union { // clang-format off @@ -94,7 +90,7 @@ bool XAudio2AudioDriver::Initialize() { assert_always(); return false; } - hr = xaudio2_create(&objects_.api_2_8.audio, 0, processor); + hr = xaudio2_create(&objects_.api_2_8.audio, 0, kProcessor); if (FAILED(hr)) { XELOGE("XAudio2Create failed with {:08X}", hr); assert_always(); @@ -102,20 +98,38 @@ bool XAudio2AudioDriver::Initialize() { } return InitializeObjects(objects_.api_2_8); } else { - hr = CoCreateInstance(__uuidof(api::XAudio2_7), NULL, CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(&objects_.api_2_7.audio)); - if (FAILED(hr)) { - XELOGE("CoCreateInstance for XAudio2 failed with {:08X}", hr); - assert_always(); + // We need to be able to accept frames from any non-STA thread - primarily + // from any guest thread, so MTA needs to be used. The AudioDriver, however, + // may be initialized from the UI thread, which has the STA concurrency + // model, or from another thread regardless of its concurrency model. So, + // all management of the objects needs to be performed in MTA. Launch the + // lifecycle management thread, which will handle initialization and + // shutdown, and also provide a scope for implicit MTA in threads that have + // never initialized COM explicitly, which lasts until all threads that have + // initialized MTA explicitly have uninitialized it - the thread that holds + // the MTA scope needs to be running while other threads are able to submit + // frames as they might have not initialized MTA explicitly. + // https://devblogs.microsoft.com/oldnewthing/?p=4613 + assert_false(mta_thread_.joinable()); + mta_thread_initialization_attempt_completed_ = false; + mta_thread_shutdown_requested_ = false; + mta_thread_ = std::thread(&XAudio2AudioDriver::MTAThread, this); + { + std::unique_lock mta_thread_initialization_completion_lock( + mta_thread_initialization_completion_mutex_); + while (true) { + if (mta_thread_initialization_attempt_completed_) { + break; + } + mta_thread_initialization_completion_cond_.wait( + mta_thread_initialization_completion_lock); + } + } + if (!mta_thread_initialization_completion_result_) { + mta_thread_.join(); return false; } - hr = objects_.api_2_7.audio->Initialize(0, processor); - if (FAILED(hr)) { - XELOGE("IXAudio2::Initialize failed with {:08X}", hr); - assert_always(); - return false; - } - return InitializeObjects(objects_.api_2_7); + return true; } } @@ -249,7 +263,16 @@ void XAudio2AudioDriver::Shutdown() { if (api_minor_version_ >= 8) { ShutdownObjects(objects_.api_2_8); } else { - ShutdownObjects(objects_.api_2_7); + // XAudio 2.7 lifecycle is managed by the MTA thread. + if (mta_thread_.joinable()) { + { + std::unique_lock mta_thread_shutdown_request_lock( + mta_thread_shutdown_request_mutex_); + mta_thread_shutdown_requested_ = true; + } + mta_thread_shutdown_request_cond_.notify_all(); + mta_thread_.join(); + } } if (xaudio2_module_) { @@ -283,6 +306,82 @@ void XAudio2AudioDriver::ShutdownObjects(Objects& objects) { } } +void XAudio2AudioDriver::MTAThread() { + xe::threading::set_name("XAudio 2.7 MTA"); + + assert_false(mta_thread_initialization_attempt_completed_); + + bool initialized = false; + + // Initializing MTA COM in this thread, as well making other (guest) threads + // that don't explicitly call CoInitializeEx implicitly MTA for the period of + // time when they can interact with XAudio 2.7 through the XAudio2AudioDriver, + // until the CoUninitialize (to be more precise, the CoUninitialize for the + // last remaining MTA thread, but we need implicit MTA for the audio here). + // https://devblogs.microsoft.com/oldnewthing/?p=4613 + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (FAILED(hr)) { + XELOGE("XAudio 2.7 MTA thread CoInitializeEx failed with {:08X}", hr); + } else { + hr = CoCreateInstance(__uuidof(api::XAudio2_7), nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&objects_.api_2_7.audio)); + if (FAILED(hr)) { + XELOGE("CoCreateInstance for XAudio2 failed with {:08X}", hr); + } else { + hr = objects_.api_2_7.audio->Initialize(0, kProcessor); + if (FAILED(hr)) { + XELOGE("IXAudio2::Initialize failed with {:08X}", hr); + } else { + if (InitializeObjects(objects_.api_2_7)) { + initialized = true; + + // Initialized successfully, await a shutdown request while keeping an + // implicit COM MTA scope. + + mta_thread_initialization_completion_result_ = true; + { + std::unique_lock + mta_thread_initialization_completion_lock( + mta_thread_initialization_completion_mutex_); + mta_thread_initialization_attempt_completed_ = true; + } + mta_thread_initialization_completion_cond_.notify_all(); + + { + std::unique_lock mta_thread_shutdown_request_lock( + mta_thread_shutdown_request_mutex_); + while (true) { + if (mta_thread_shutdown_requested_) { + break; + } + mta_thread_shutdown_request_cond_.wait( + mta_thread_shutdown_request_lock); + } + } + } + + // Even if InitializeObjects has failed, need to clean up with + // ShutdownObjects. + ShutdownObjects(objects_.api_2_7); + } + } + CoUninitialize(); + } + + if (!initialized) { + mta_thread_initialization_completion_result_ = false; + { + // Failed to initialize - notify the threads waiting for the + // initialization. + std::unique_lock mta_thread_initialization_completion_lock( + mta_thread_initialization_completion_mutex_); + mta_thread_initialization_attempt_completed_ = true; + } + mta_thread_initialization_completion_cond_.notify_all(); + } +} + } // namespace xaudio2 } // namespace apu } // namespace xe diff --git a/src/xenia/apu/xaudio2/xaudio2_audio_driver.h b/src/xenia/apu/xaudio2/xaudio2_audio_driver.h index 42d4b59a9..accfdd56a 100644 --- a/src/xenia/apu/xaudio2/xaudio2_audio_driver.h +++ b/src/xenia/apu/xaudio2/xaudio2_audio_driver.h @@ -10,8 +10,13 @@ #ifndef XENIA_APU_XAUDIO2_XAUDIO2_AUDIO_DRIVER_H_ #define XENIA_APU_XAUDIO2_XAUDIO2_AUDIO_DRIVER_H_ +#include +#include +#include + #include "xenia/apu/audio_driver.h" #include "xenia/apu/xaudio2/xaudio2_api.h" +#include "xenia/base/platform.h" #include "xenia/base/threading.h" struct IXAudio2; @@ -28,17 +33,45 @@ class XAudio2AudioDriver : public AudioDriver { ~XAudio2AudioDriver() override; bool Initialize(); + // Must not be called from COM STA threads as MTA XAudio 2.7 may be used. It's + // fine to call this from threads that have never initialized COM as + // initializing MTA for any thread implicitly initializes it for all threads + // not explicitly requesting STA (until COM is uninitialized all threads that + // have initialized MTA). + // https://devblogs.microsoft.com/oldnewthing/?p=4613 void SubmitFrame(uint32_t frame_ptr) override; void Shutdown(); private: + // First CPU (2.8 default). XAUDIO2_ANY_PROCESSOR (2.7 default) steals too + // much time from other things. Ideally should process audio on what roughly + // represents thread 4 (5th) on the Xbox 360 (2.7 default on the console), or + // even beyond the 6 guest cores. + api::XAUDIO2_PROCESSOR kProcessor = 0x00000001; + + // For XAudio 2.7, InitializeObjects and ShutdownObjects must be called only + // in the lifecycle management thread with COM MTA initialized. template bool InitializeObjects(Objects& objects); template void ShutdownObjects(Objects& objects); + void MTAThread(); + void* xaudio2_module_ = nullptr; uint32_t api_minor_version_ = 7; + + bool mta_thread_initialization_completion_result_; + std::mutex mta_thread_initialization_completion_mutex_; + std::condition_variable mta_thread_initialization_completion_cond_; + bool mta_thread_initialization_attempt_completed_; + + std::mutex mta_thread_shutdown_request_mutex_; + std::condition_variable mta_thread_shutdown_request_cond_; + bool mta_thread_shutdown_requested_; + + std::thread mta_thread_; + union { struct { api::IXAudio2_7* audio; diff --git a/src/xenia/base/app_win32.manifest b/src/xenia/base/app_win32.manifest new file mode 100644 index 000000000..acba0ff0e --- /dev/null +++ b/src/xenia/base/app_win32.manifest @@ -0,0 +1,14 @@ + + + + + + + + + + True/PM + PerMonitor + + + diff --git a/src/xenia/base/console.h b/src/xenia/base/console.h new file mode 100644 index 000000000..8f7f26728 --- /dev/null +++ b/src/xenia/base/console.h @@ -0,0 +1,22 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_BASE_CONSOLE_H_ +#define XENIA_BASE_CONSOLE_H_ + +namespace xe { + +// Returns true if there is a user-visible console attached to receive stdout. +bool has_console_attached(); + +void AttachConsole(); + +} // namespace xe + +#endif // XENIA_BASE_CONSOLE_H_ diff --git a/src/xenia/base/main.h b/src/xenia/base/console_app_main.h similarity index 51% rename from src/xenia/base/main.h rename to src/xenia/base/console_app_main.h index 324abfe71..1db83cf60 100644 --- a/src/xenia/base/main.h +++ b/src/xenia/base/console_app_main.h @@ -2,54 +2,50 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2020 Ben Vanik. All rights reserved. * + * Copyright 2021 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ -#ifndef XENIA_BASE_MAIN_H_ -#define XENIA_BASE_MAIN_H_ +#ifndef XENIA_BASE_CONSOLE_APP_MAIN_H_ +#define XENIA_BASE_CONSOLE_APP_MAIN_H_ -#include #include #include -#include "xenia/base/cvar.h" -#include "xenia/base/platform.h" - namespace xe { -// Returns true if there is a user-visible console attached to receive stdout. -bool has_console_attached(); - -void AttachConsole(); - // Extern defined by user code. This must be present for the application to // launch. -struct EntryInfo { +struct ConsoleAppEntryInfo { std::string name; int (*entry_point)(const std::vector& args); bool transparent_options; // no argument parsing - std::optional positional_usage; - std::optional> positional_options; + std::string positional_usage; + std::vector positional_options; }; -EntryInfo GetEntryInfo(); +ConsoleAppEntryInfo GetConsoleAppEntryInfo(); -#define DEFINE_ENTRY_POINT(name, entry_point, positional_usage, ...) \ - xe::EntryInfo xe::GetEntryInfo() { \ +// TODO(Triang3l): Multiple console app entry points on Android when a console +// activity is added. This is currently for individual executables running on +// Termux. + +#define XE_DEFINE_CONSOLE_APP(name, entry_point, positional_usage, ...) \ + xe::ConsoleAppEntryInfo xe::GetConsoleAppEntryInfo() { \ std::initializer_list positional_options = {__VA_ARGS__}; \ - return xe::EntryInfo{ \ + return xe::ConsoleAppEntryInfo{ \ name, entry_point, false, positional_usage, \ std::vector(std::move(positional_options))}; \ } // TODO(Joel Linn): Add some way to filter consumed arguments in // cvar::ParseLaunchArguments() -#define DEFINE_ENTRY_POINT_TRANSPARENT(name, entry_point) \ - xe::EntryInfo xe::GetEntryInfo() { \ - return xe::EntryInfo{name, entry_point, true, std::nullopt, std::nullopt}; \ +#define XE_DEFINE_CONSOLE_APP_TRANSPARENT(name, entry_point) \ + xe::ConsoleAppEntryInfo xe::GetConsoleAppEntryInfo() { \ + return xe::ConsoleAppEntryInfo{name, entry_point, true, std::string(), \ + std::vector()}; \ } } // namespace xe -#endif // XENIA_BASE_MAIN_H_ +#endif // XENIA_BASE_CONSOLE_APP_MAIN_H_ diff --git a/src/xenia/base/console_app_main_android.cc b/src/xenia/base/console_app_main_android.cc new file mode 100644 index 000000000..70e69fc5e --- /dev/null +++ b/src/xenia/base/console_app_main_android.cc @@ -0,0 +1,10 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/base/console_app_posix.cc" diff --git a/src/xenia/base/main_posix.cc b/src/xenia/base/console_app_main_posix.cc similarity index 64% rename from src/xenia/base/main_posix.cc rename to src/xenia/base/console_app_main_posix.cc index d3f0ef403..29a3ae6ab 100644 --- a/src/xenia/base/main_posix.cc +++ b/src/xenia/base/console_app_main_posix.cc @@ -7,42 +7,32 @@ ****************************************************************************** */ -#include -#include +#include +#include +#include "xenia/base/console_app_main.h" #include "xenia/base/cvar.h" -#include "xenia/base/main.h" - -#include "xenia/base/filesystem.h" #include "xenia/base/logging.h" -#include "xenia/base/string.h" - -namespace xe { - -bool has_console_attached() { return isatty(fileno(stdin)) == 1; } - -void AttachConsole() {} - -} // namespace xe extern "C" int main(int argc, char** argv) { - auto entry_info = xe::GetEntryInfo(); + xe::ConsoleAppEntryInfo entry_info = xe::GetConsoleAppEntryInfo(); if (!entry_info.transparent_options) { - cvar::ParseLaunchArguments(argc, argv, entry_info.positional_usage.value(), - entry_info.positional_options.value()); + cvar::ParseLaunchArguments(argc, argv, entry_info.positional_usage, + entry_info.positional_options); } + // Initialize logging. Needs parsed cvars. + xe::InitializeLogging(entry_info.name); + std::vector args; for (int n = 0; n < argc; n++) { - args.push_back(argv[n]); + args.emplace_back(argv[n]); } - // Initialize logging. Needs parsed FLAGS. - xe::InitializeLogging(entry_info.name); - - // Call app-provided entry point. int result = entry_info.entry_point(args); + xe::ShutdownLogging(); + return result; } diff --git a/src/xenia/base/console_app_main_win.cc b/src/xenia/base/console_app_main_win.cc new file mode 100644 index 000000000..cd2a38dc4 --- /dev/null +++ b/src/xenia/base/console_app_main_win.cc @@ -0,0 +1,36 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include +#include + +#include "xenia/base/console_app_main.h" +#include "xenia/base/main_win.h" + +int main(int argc_ignored, char** argv_ignored) { + xe::ConsoleAppEntryInfo entry_info = xe::GetConsoleAppEntryInfo(); + + std::vector args; + if (!xe::ParseWin32LaunchArguments(entry_info.transparent_options, + entry_info.positional_usage, + entry_info.positional_options, &args)) { + return EXIT_FAILURE; + } + + int result = xe::InitializeWin32App(entry_info.name); + if (result) { + return result; + } + + result = entry_info.entry_point(args); + + xe::ShutdownWin32App(); + + return result; +} diff --git a/src/xenia/base/console_posix.cc b/src/xenia/base/console_posix.cc new file mode 100644 index 000000000..0f8cf38f9 --- /dev/null +++ b/src/xenia/base/console_posix.cc @@ -0,0 +1,21 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include +#include + +#include "xenia/base/console.h" + +namespace xe { + +bool has_console_attached() { return isatty(fileno(stdin)) == 1; } + +void AttachConsole() {} + +} // namespace xe diff --git a/src/xenia/base/console_win.cc b/src/xenia/base/console_win.cc new file mode 100644 index 000000000..e6b8fb887 --- /dev/null +++ b/src/xenia/base/console_win.cc @@ -0,0 +1,60 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include +#include +#include +#include + +#include "xenia/base/console.h" +#include "xenia/base/platform_win.h" + +namespace xe { + +// TODO(Triang3l): Set the default depending on the actual subsystem. Currently +// it inhibits message boxes. +static bool has_console_attached_ = true; + +bool has_console_attached() { return has_console_attached_; } + +static bool has_shell_environment_variable() { + size_t size = 0; + // Check if SHELL exists + // If it doesn't, then we are in a Windows Terminal + auto error = getenv_s(&size, nullptr, 0, "SHELL"); + if (error) { + return false; + } + return !!size; +} + +void AttachConsole() { + bool has_console = ::AttachConsole(ATTACH_PARENT_PROCESS) == TRUE; + if (!has_console || !has_shell_environment_variable()) { + // We weren't launched from a console, so just return. + has_console_attached_ = false; + return; + } + + AllocConsole(); + + has_console_attached_ = true; + + auto std_handle = (intptr_t)GetStdHandle(STD_OUTPUT_HANDLE); + auto con_handle = _open_osfhandle(std_handle, _O_TEXT); + auto fp = _fdopen(con_handle, "w"); + freopen_s(&fp, "CONOUT$", "w", stdout); + + std_handle = (intptr_t)GetStdHandle(STD_ERROR_HANDLE); + con_handle = _open_osfhandle(std_handle, _O_TEXT); + fp = _fdopen(con_handle, "w"); + freopen_s(&fp, "CONOUT$", "w", stderr); +} + +} // namespace xe diff --git a/src/xenia/base/cvar.cc b/src/xenia/base/cvar.cc index cd85c7a68..95c7a58f3 100644 --- a/src/xenia/base/cvar.cc +++ b/src/xenia/base/cvar.cc @@ -14,8 +14,8 @@ #define UTF_CPP_CPLUSPLUS 201703L #include "third_party/utfcpp/source/utf8.h" +#include "xenia/base/console.h" #include "xenia/base/logging.h" -#include "xenia/base/main.h" #include "xenia/base/system.h" namespace utfcpp = utf8; diff --git a/src/xenia/base/logging.cc b/src/xenia/base/logging.cc index 02fa787a4..b78ad88e3 100644 --- a/src/xenia/base/logging.cc +++ b/src/xenia/base/logging.cc @@ -18,10 +18,10 @@ #include "third_party/disruptorplus/include/disruptorplus/sequence_barrier.hpp" #include "third_party/disruptorplus/include/disruptorplus/spin_wait_strategy.hpp" #include "xenia/base/atomic.h" +#include "xenia/base/console.h" #include "xenia/base/cvar.h" #include "xenia/base/debugging.h" #include "xenia/base/filesystem.h" -#include "xenia/base/main.h" #include "xenia/base/math.h" #include "xenia/base/memory.h" #include "xenia/base/ring_buffer.h" diff --git a/src/xenia/base/main_init_android.cc b/src/xenia/base/main_init_android.cc new file mode 100644 index 000000000..28b976731 --- /dev/null +++ b/src/xenia/base/main_init_android.cc @@ -0,0 +1,10 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +// Nothing. Stub. diff --git a/src/xenia/base/main_init_posix.cc b/src/xenia/base/main_init_posix.cc index 4a426a196..28b976731 100644 --- a/src/xenia/base/main_init_posix.cc +++ b/src/xenia/base/main_init_posix.cc @@ -2,13 +2,9 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2014 Ben Vanik. All rights reserved. * + * Copyright 2021 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ -#include "xenia/base/main.h" - -#include - // Nothing. Stub. diff --git a/src/xenia/base/main_init_win.cc b/src/xenia/base/main_init_win.cc index e6b0c9c59..6b0a9059a 100644 --- a/src/xenia/base/main_init_win.cc +++ b/src/xenia/base/main_init_win.cc @@ -7,10 +7,11 @@ ****************************************************************************** */ -#include "xenia/base/main.h" +#include "xenia/base/platform_win.h" #include +// Includes Windows headers, so it goes after platform_win.h. #include "third_party/xbyak/xbyak/xbyak_util.h" class StartupAvxCheck { diff --git a/src/xenia/base/main_win.cc b/src/xenia/base/main_win.cc index 5d68811f1..33bd65e75 100644 --- a/src/xenia/base/main_win.cc +++ b/src/xenia/base/main_win.cc @@ -7,14 +7,12 @@ ****************************************************************************** */ -#include -#include - -#include +#include +#include #include "xenia/base/cvar.h" #include "xenia/base/logging.h" -#include "xenia/base/main.h" +#include "xenia/base/main_win.h" #include "xenia/base/platform_win.h" #include "xenia/base/string.h" @@ -24,54 +22,11 @@ // For RequestHighPerformance. #include -// Includes Windows headers, so it goes here. -#include "third_party/xbyak/xbyak/xbyak_util.h" - DEFINE_bool(win32_high_freq, true, "Requests high performance from the NT kernel", "Kernel"); -DEFINE_bool(enable_console, false, "Open a console window with the main window", - "General"); namespace xe { -bool has_console_attached_ = true; - -bool has_console_attached() { return has_console_attached_; } - -bool has_shell_environment_variable() { - size_t size = 0; - // Check if SHELL exists - // If it doesn't, then we are in a Windows Terminal - auto error = getenv_s(&size, nullptr, 0, "SHELL"); - if (error) { - return false; - } - return !!size; -} - -void AttachConsole() { - bool has_console = ::AttachConsole(ATTACH_PARENT_PROCESS) == TRUE; - if (!has_console || !has_shell_environment_variable()) { - // We weren't launched from a console, so just return. - has_console_attached_ = false; - return; - } - - AllocConsole(); - - has_console_attached_ = true; - - auto std_handle = (intptr_t)GetStdHandle(STD_OUTPUT_HANDLE); - auto con_handle = _open_osfhandle(std_handle, _O_TEXT); - auto fp = _fdopen(con_handle, "w"); - freopen_s(&fp, "CONOUT$", "w", stdout); - - std_handle = (intptr_t)GetStdHandle(STD_ERROR_HANDLE); - con_handle = _open_osfhandle(std_handle, _O_TEXT); - fp = _fdopen(con_handle, "w"); - freopen_s(&fp, "CONOUT$", "w", stderr); -} - static void RequestHighPerformance() { #if XE_PLATFORM_WIN32 NTSTATUS(*NtQueryTimerResolution) @@ -97,8 +52,10 @@ static void RequestHighPerformance() { #endif } -static bool parse_launch_arguments(const xe::EntryInfo& entry_info, - std::vector& args) { +bool ParseWin32LaunchArguments( + bool transparent_options, const std::string_view positional_usage, + const std::vector& positional_options, + std::vector* args_out) { auto command_line = GetCommandLineW(); int wargc; @@ -118,48 +75,24 @@ static bool parse_launch_arguments(const xe::EntryInfo& entry_info, LocalFree(wargv); - if (!entry_info.transparent_options) { - cvar::ParseLaunchArguments(argc, argv, entry_info.positional_usage.value(), - entry_info.positional_options.value()); + if (!transparent_options) { + cvar::ParseLaunchArguments(argc, argv, positional_usage, + positional_options); } - args.clear(); - for (int n = 0; n < argc; n++) { - args.push_back(std::string(argv[n])); + if (args_out) { + args_out->clear(); + for (int n = 0; n < argc; n++) { + args_out->push_back(std::string(argv[n])); + } } return true; } -int Main() { - auto entry_info = xe::GetEntryInfo(); - - std::vector args; - if (!parse_launch_arguments(entry_info, args)) { - return 1; - } - - // Attach a console so we can write output to stdout. If the user hasn't - // redirected output themselves it'll pop up a window. - if (cvars::enable_console) { - xe::AttachConsole(); - } - - // Setup COM on the main thread. - // NOTE: this may fail if COM has already been initialized - that's OK. -#pragma warning(suppress : 6031) - CoInitializeEx(nullptr, COINIT_MULTITHREADED); - +int InitializeWin32App(const std::string_view app_name) { // Initialize logging. Needs parsed FLAGS. - xe::InitializeLogging(entry_info.name); - - Xbyak::util::Cpu cpu; - if (!cpu.has(Xbyak::util::Cpu::tAVX)) { - xe::FatalError( - "Your CPU does not support AVX, which is required by Xenia. See the " - "FAQ for system requirements at https://xenia.jp"); - return -1; - } + xe::InitializeLogging(app_name); // Print version info. XELOGI( @@ -175,38 +108,9 @@ int Main() { RequestHighPerformance(); } - // Call app-provided entry point. - int result = entry_info.entry_point(args); - - xe::ShutdownLogging(); - return result; + return 0; } +void ShutdownWin32App() { xe::ShutdownLogging(); } + } // namespace xe - -// Used in console mode apps; automatically picked based on subsystem. -int main(int argc_ignored, char** argv_ignored) { return xe::Main(); } - -// Used in windowed apps; automatically picked based on subsystem. -int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR command_line, int) { - // Run normal entry point. - return xe::Main(); -} - -#if defined _M_IX86 -#pragma comment( \ - linker, \ - "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"") // NOLINT(whitespace/line_length) -#elif defined _M_IA64 -#pragma comment( \ - linker, \ - "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='ia64' publicKeyToken='6595b64144ccf1df' language='*'\"") // NOLINT(whitespace/line_length) -#elif defined _M_X64 -#pragma comment( \ - linker, \ - "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"") // NOLINT(whitespace/line_length) -#else -#pragma comment( \ - linker, \ - "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") // NOLINT(whitespace/line_length) -#endif diff --git a/src/xenia/base/main_win.h b/src/xenia/base/main_win.h new file mode 100644 index 000000000..87a07b272 --- /dev/null +++ b/src/xenia/base/main_win.h @@ -0,0 +1,29 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_BASE_MAIN_WIN_H_ +#define XENIA_BASE_MAIN_WIN_H_ + +#include +#include + +namespace xe { + +// Functions for calling in both windowed and console entry points. +bool ParseWin32LaunchArguments( + bool transparent_options, const std::string_view positional_usage, + const std::vector& positional_options, + std::vector* args_out); +// InitializeWin32App uses cvars, call ParseWin32LaunchArguments before. +int InitializeWin32App(const std::string_view app_name); +void ShutdownWin32App(); + +} // namespace xe + +#endif // XENIA_BASE_MAIN_WIN_H_ diff --git a/src/xenia/base/premake5.lua b/src/xenia/base/premake5.lua index f58bf5558..d53c34351 100644 --- a/src/xenia/base/premake5.lua +++ b/src/xenia/base/premake5.lua @@ -11,7 +11,8 @@ project("xenia-base") defines({ }) local_platform_files() - removefiles({"main_*.cc"}) + removefiles({"console_app_main_*.cc"}) + removefiles({"main_init_*.cc"}) files({ "debug_visualizers.natvis", }) diff --git a/src/xenia/cpu/ppc/testing/ppc_testing_main.cc b/src/xenia/cpu/ppc/testing/ppc_testing_main.cc index 1d115af1e..b2da8aff7 100644 --- a/src/xenia/cpu/ppc/testing/ppc_testing_main.cc +++ b/src/xenia/cpu/ppc/testing/ppc_testing_main.cc @@ -7,10 +7,10 @@ ****************************************************************************** */ +#include "xenia/base/console_app_main.h" #include "xenia/base/cvar.h" #include "xenia/base/filesystem.h" #include "xenia/base/logging.h" -#include "xenia/base/main.h" #include "xenia/base/math.h" #include "xenia/base/platform.h" #include "xenia/base/string_buffer.h" @@ -483,5 +483,5 @@ int main(const std::vector& args) { } // namespace cpu } // namespace xe -DEFINE_ENTRY_POINT("xenia-cpu-ppc-test", xe::cpu::test::main, "[test name]", - "test_name"); +XE_DEFINE_CONSOLE_APP("xenia-cpu-ppc-test", xe::cpu::test::main, "[test name]", + "test_name"); diff --git a/src/xenia/cpu/ppc/testing/ppc_testing_native_main.cc b/src/xenia/cpu/ppc/testing/ppc_testing_native_main.cc index 074552d36..689544316 100644 --- a/src/xenia/cpu/ppc/testing/ppc_testing_native_main.cc +++ b/src/xenia/cpu/ppc/testing/ppc_testing_native_main.cc @@ -7,6 +7,8 @@ ****************************************************************************** */ +#include "xenia/base/console_app_main.h" +#include "xenia/base/cvar.h" #include "xenia/base/filesystem.h" #include "xenia/base/logging.h" #include "xenia/base/main.h" @@ -23,6 +25,7 @@ DEFINE_string(test_path, "src/xenia/cpu/ppc/testing/", "Directory scanned for test files."); DEFINE_string(test_bin_path, "src/xenia/cpu/ppc/testing/bin/", "Directory with binary outputs of the test files."); +DEFINE_transient_string(test_name, "", "Test suite name.", "General"); extern "C" void xe_call_native(void* context, void* fn); @@ -505,19 +508,13 @@ bool RunTests(const std::wstring& test_name) { return failed_count ? false : true; } -int main(const std::vector& args) { - // Grab test name, if present. - std::wstring test_name; - if (args.size() >= 2) { - test_name = args[1]; - } - - return RunTests(test_name) ? 0 : 1; +int main(const std::vector& args) { + return RunTests(cvars::test_name) ? 0 : 1; } } // namespace test } // namespace cpu } // namespace xe -DEFINE_ENTRY_POINT(L"xenia-cpu-ppc-test", L"xenia-cpu-ppc-test [test name]", - xe::cpu::test::main); +XE_DEFINE_CONSOLE_APP("xenia-cpu-ppc-test", xe::cpu::test::main, "[test name]", + "test_name"); diff --git a/src/xenia/cpu/ppc/testing/premake5.lua b/src/xenia/cpu/ppc/testing/premake5.lua index b9c007b27..d91256460 100644 --- a/src/xenia/cpu/ppc/testing/premake5.lua +++ b/src/xenia/cpu/ppc/testing/premake5.lua @@ -17,7 +17,7 @@ project("xenia-cpu-ppc-tests") }) files({ "ppc_testing_main.cc", - "../../../base/main_"..platform_suffix..".cc", + "../../../base/console_app_main_"..platform_suffix..".cc", }) files({ "*.s", @@ -46,7 +46,7 @@ project("xenia-cpu-ppc-nativetests") }) files({ "ppc_testing_native_main.cc", - "../../../base/main_"..platform_suffix..".cc", + "../../../base/console_app_main_"..platform_suffix..".cc", }) files({ "instr_*.s", diff --git a/src/xenia/cpu/testing/sandbox_main.cc b/src/xenia/cpu/testing/sandbox_main.cc index 0998ddefe..a126c1677 100644 --- a/src/xenia/cpu/testing/sandbox_main.cc +++ b/src/xenia/cpu/testing/sandbox_main.cc @@ -7,7 +7,7 @@ ****************************************************************************** */ -#include "xenia/base/main.h" +#include "xenia/base/console_app_main.h" #include "xenia/cpu/backend/x64/x64_backend.h" #include "xenia/cpu/cpu.h" #include "xenia/cpu/ppc/ppc_context.h" @@ -23,10 +23,10 @@ using xe::cpu::ppc::PPCContext; // TODO(benvanik): simple memory? move more into core? -int main(std::vector& args) { +int main(std::vector& args) { #if XE_OPTION_PROFILING xe::Profiler::Initialize(); - xe::Profiler::ThreadEnter("main"); + xe::Profiler::ThreadEnter("Main"); #endif // XE_OPTION_PROFILING size_t memory_size = 16 * 1024 * 1024; @@ -79,4 +79,4 @@ int main(std::vector& args) { } // namespace cpu } // namespace xe -DEFINE_ENTRY_POINT(L"xenia-cpu-sandbox", L"?", xe::cpu::sandbox::main); +XE_DEFINE_CONSOLE_APP("xenia-cpu-sandbox", xe::cpu::sandbox::main, ""); diff --git a/src/xenia/cpu/testing/util.h b/src/xenia/cpu/testing/util.h index 79ca8bc03..d5c89059c 100644 --- a/src/xenia/cpu/testing/util.h +++ b/src/xenia/cpu/testing/util.h @@ -12,7 +12,6 @@ #include -#include "xenia/base/main.h" #include "xenia/cpu/backend/x64/x64_backend.h" #include "xenia/cpu/hir/hir_builder.h" #include "xenia/cpu/ppc/ppc_context.h" diff --git a/src/xenia/debug/ui/debug_window.cc b/src/xenia/debug/ui/debug_window.cc index 967646f9c..bb9ee8225 100644 --- a/src/xenia/debug/ui/debug_window.cc +++ b/src/xenia/debug/ui/debug_window.cc @@ -32,6 +32,7 @@ #include "xenia/kernel/xthread.h" #include "xenia/ui/graphics_provider.h" #include "xenia/ui/imgui_drawer.h" +#include "xenia/ui/windowed_app_context.h" DEFINE_bool(imgui_debug, false, "Show ImGui debugging tools.", "UI"); @@ -50,11 +51,12 @@ using xe::ui::UIEvent; const std::string kBaseTitle = "Xenia Debugger"; -DebugWindow::DebugWindow(Emulator* emulator, xe::ui::Loop* loop) +DebugWindow::DebugWindow(Emulator* emulator, + xe::ui::WindowedAppContext& app_context) : emulator_(emulator), processor_(emulator->processor()), - loop_(loop), - window_(xe::ui::Window::Create(loop_, kBaseTitle)) { + app_context_(app_context), + window_(xe::ui::Window::Create(app_context_, kBaseTitle)) { if (cs_open(CS_ARCH_X86, CS_MODE_64, &capstone_handle_) != CS_ERR_OK) { assert_always("Failed to initialize capstone"); } @@ -63,16 +65,18 @@ DebugWindow::DebugWindow(Emulator* emulator, xe::ui::Loop* loop) } DebugWindow::~DebugWindow() { - loop_->PostSynchronous([this]() { window_.reset(); }); + // Make sure pending functions referencing the DebugWindow are executed. + app_context_.ExecutePendingFunctionsFromUIThread(); if (capstone_handle_) { cs_close(&capstone_handle_); } } -std::unique_ptr DebugWindow::Create(Emulator* emulator, - xe::ui::Loop* loop) { - std::unique_ptr debug_window(new DebugWindow(emulator, loop)); +std::unique_ptr DebugWindow::Create( + Emulator* emulator, xe::ui::WindowedAppContext& app_context) { + std::unique_ptr debug_window( + new DebugWindow(emulator, app_context)); if (!debug_window->Initialize()) { xe::FatalError("Failed to initialize debug window"); return nullptr; @@ -87,8 +91,6 @@ bool DebugWindow::Initialize() { return false; } - loop_->on_quit.AddListener([this](UIEvent* e) { window_.reset(); }); - // Main menu. auto main_menu = MenuItem::Create(MenuItem::Type::kNormal); auto file_menu = MenuItem::Create(MenuItem::Type::kPopup, "&File"); @@ -1425,7 +1427,7 @@ void DebugWindow::UpdateCache() { auto kernel_state = emulator_->kernel_state(); auto object_table = kernel_state->object_table(); - loop_->Post([this]() { + app_context_.CallInUIThread([this]() { std::string title = kBaseTitle; switch (processor_->execution_state()) { case cpu::ExecutionState::kEnded: @@ -1531,9 +1533,7 @@ Breakpoint* DebugWindow::LookupBreakpointAtAddress( } } -void DebugWindow::OnFocus() { - loop_->Post([this]() { window_->set_focus(true); }); -} +void DebugWindow::OnFocus() { Focus(); } void DebugWindow::OnDetached() { UpdateCache(); @@ -1546,30 +1546,34 @@ void DebugWindow::OnDetached() { void DebugWindow::OnExecutionPaused() { UpdateCache(); - loop_->Post([this]() { window_->set_focus(true); }); + Focus(); } void DebugWindow::OnExecutionContinued() { UpdateCache(); - loop_->Post([this]() { window_->set_focus(true); }); + Focus(); } void DebugWindow::OnExecutionEnded() { UpdateCache(); - loop_->Post([this]() { window_->set_focus(true); }); + Focus(); } void DebugWindow::OnStepCompleted(cpu::ThreadDebugInfo* thread_info) { UpdateCache(); SelectThreadStackFrame(thread_info, 0, true); - loop_->Post([this]() { window_->set_focus(true); }); + Focus(); } void DebugWindow::OnBreakpointHit(Breakpoint* breakpoint, cpu::ThreadDebugInfo* thread_info) { UpdateCache(); SelectThreadStackFrame(thread_info, 0, true); - loop_->Post([this]() { window_->set_focus(true); }); + Focus(); +} + +void DebugWindow::Focus() const { + app_context_.CallInUIThread([this]() { window_->set_focus(true); }); } } // namespace ui diff --git a/src/xenia/debug/ui/debug_window.h b/src/xenia/debug/ui/debug_window.h index 5a0a5844b..7c9cfae4f 100644 --- a/src/xenia/debug/ui/debug_window.h +++ b/src/xenia/debug/ui/debug_window.h @@ -18,9 +18,9 @@ #include "xenia/cpu/debug_listener.h" #include "xenia/cpu/processor.h" #include "xenia/emulator.h" -#include "xenia/ui/loop.h" #include "xenia/ui/menu_item.h" #include "xenia/ui/window.h" +#include "xenia/ui/windowed_app_context.h" #include "xenia/xbox.h" namespace xe { @@ -31,11 +31,11 @@ class DebugWindow : public cpu::DebugListener { public: ~DebugWindow(); - static std::unique_ptr Create(Emulator* emulator, - xe::ui::Loop* loop); + static std::unique_ptr Create( + Emulator* emulator, xe::ui::WindowedAppContext& app_context); Emulator* emulator() const { return emulator_; } - xe::ui::Loop* loop() const { return loop_; } + xe::ui::WindowedAppContext& app_context() const { return app_context_; } xe::ui::Window* window() const { return window_.get(); } void OnFocus() override; @@ -48,7 +48,8 @@ class DebugWindow : public cpu::DebugListener { cpu::ThreadDebugInfo* thread_info) override; private: - explicit DebugWindow(Emulator* emulator, xe::ui::Loop* loop); + explicit DebugWindow(Emulator* emulator, + xe::ui::WindowedAppContext& app_context); bool Initialize(); void DrawFrame(); @@ -86,9 +87,11 @@ class DebugWindow : public cpu::DebugListener { cpu::Breakpoint* LookupBreakpointAtAddress( cpu::Breakpoint::AddressType address_type, uint64_t address); + void Focus() const; + Emulator* emulator_ = nullptr; cpu::Processor* processor_ = nullptr; - xe::ui::Loop* loop_ = nullptr; + xe::ui::WindowedAppContext& app_context_; std::unique_ptr window_; uint64_t last_draw_tick_count_ = 0; diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 92ddabe4f..7bb4109e4 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -40,6 +40,8 @@ #include "xenia/kernel/xboxkrnl/xboxkrnl_module.h" #include "xenia/memory.h" #include "xenia/ui/imgui_dialog.h" +#include "xenia/ui/window.h" +#include "xenia/ui/windowed_app_context.h" #include "xenia/vfs/devices/disc_image_device.h" #include "xenia/vfs/devices/host_path_device.h" #include "xenia/vfs/devices/null_device.h" @@ -233,7 +235,7 @@ X_STATUS Emulator::Setup( if (display_window_) { // Finish initializing the display. - display_window_->loop()->PostSynchronous([this]() { + display_window_->app_context().CallInUIThreadSynchronous([this]() { xe::ui::GraphicsContextLock context_lock(display_window_->context()); Profiler::set_window(display_window_); }); @@ -583,7 +585,7 @@ bool Emulator::ExceptionCallback(Exception* ex) { } // Display a dialog telling the user the guest has crashed. - display_window()->loop()->PostSynchronous([&]() { + display_window()->app_context().CallInUIThreadSynchronous([this]() { xe::ui::ImGuiDialog::ShowMessageBox( display_window(), "Uh-oh!", "The guest has crashed.\n\n" diff --git a/src/xenia/gpu/d3d12/d3d12_graphics_system.cc b/src/xenia/gpu/d3d12/d3d12_graphics_system.cc index a0cc6fab6..411e95ce5 100644 --- a/src/xenia/gpu/d3d12/d3d12_graphics_system.cc +++ b/src/xenia/gpu/d3d12/d3d12_graphics_system.cc @@ -49,7 +49,7 @@ std::string D3D12GraphicsSystem::name() const { X_STATUS D3D12GraphicsSystem::Setup(cpu::Processor* processor, kernel::KernelState* kernel_state, ui::Window* target_window) { - provider_ = xe::ui::d3d12::D3D12Provider::Create(target_window); + provider_ = xe::ui::d3d12::D3D12Provider::Create(); auto d3d12_provider = static_cast(provider()); auto device = d3d12_provider->GetDevice(); diff --git a/src/xenia/gpu/d3d12/d3d12_trace_dump_main.cc b/src/xenia/gpu/d3d12/d3d12_trace_dump_main.cc index 127a2226f..eaa25ddd9 100644 --- a/src/xenia/gpu/d3d12/d3d12_trace_dump_main.cc +++ b/src/xenia/gpu/d3d12/d3d12_trace_dump_main.cc @@ -7,8 +7,8 @@ ****************************************************************************** */ +#include "xenia/base/console_app_main.h" #include "xenia/base/logging.h" -#include "xenia/base/main.h" #include "xenia/gpu/d3d12/d3d12_command_processor.h" #include "xenia/gpu/d3d12/d3d12_graphics_system.h" #include "xenia/gpu/trace_dump.h" @@ -54,6 +54,6 @@ int trace_dump_main(const std::vector& args) { } // namespace gpu } // namespace xe -DEFINE_ENTRY_POINT("xenia-gpu-d3d12-trace-dump", - xe::gpu::d3d12::trace_dump_main, "some.trace", - "target_trace_file"); +XE_DEFINE_CONSOLE_APP("xenia-gpu-d3d12-trace-dump", + xe::gpu::d3d12::trace_dump_main, "some.trace", + "target_trace_file"); diff --git a/src/xenia/gpu/d3d12/d3d12_trace_viewer_main.cc b/src/xenia/gpu/d3d12/d3d12_trace_viewer_main.cc index 481035d7c..71b3fc403 100644 --- a/src/xenia/gpu/d3d12/d3d12_trace_viewer_main.cc +++ b/src/xenia/gpu/d3d12/d3d12_trace_viewer_main.cc @@ -2,13 +2,15 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2020 Ben Vanik. All rights reserved. * + * Copyright 2021 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ +#include +#include + #include "xenia/base/logging.h" -#include "xenia/base/main.h" #include "xenia/gpu/d3d12/d3d12_command_processor.h" #include "xenia/gpu/d3d12/d3d12_graphics_system.h" #include "xenia/gpu/trace_viewer.h" @@ -17,10 +19,13 @@ namespace xe { namespace gpu { namespace d3d12 { -using namespace xe::gpu::xenos; - -class D3D12TraceViewer : public TraceViewer { +class D3D12TraceViewer final : public TraceViewer { public: + static std::unique_ptr Create( + xe::ui::WindowedAppContext& app_context) { + return std::unique_ptr(new D3D12TraceViewer(app_context)); + } + std::unique_ptr CreateGraphicsSystem() override { return std::unique_ptr(new D3D12GraphicsSystem()); } @@ -45,17 +50,15 @@ class D3D12TraceViewer : public TraceViewer { // TextureInfo/SamplerInfo which are going away. return 0; } -}; -int trace_viewer_main(const std::vector& args) { - D3D12TraceViewer trace_viewer; - return trace_viewer.Main(args); -} + private: + explicit D3D12TraceViewer(xe::ui::WindowedAppContext& app_context) + : TraceViewer(app_context, "xenia-gpu-d3d12-trace-viewer") {} +}; } // namespace d3d12 } // namespace gpu } // namespace xe -DEFINE_ENTRY_POINT("xenia-gpu-d3d12-trace-viewer", - xe::gpu::d3d12::trace_viewer_main, "some.trace", - "target_trace_file"); +XE_DEFINE_WINDOWED_APP(xenia_gpu_d3d12_trace_viewer, + xe::gpu::d3d12::D3D12TraceViewer::Create); diff --git a/src/xenia/gpu/d3d12/premake5.lua b/src/xenia/gpu/d3d12/premake5.lua index fd1baf654..bc6e95653 100644 --- a/src/xenia/gpu/d3d12/premake5.lua +++ b/src/xenia/gpu/d3d12/premake5.lua @@ -54,7 +54,7 @@ project("xenia-gpu-d3d12-trace-viewer") }) files({ "d3d12_trace_viewer_main.cc", - "../../base/main_"..platform_suffix..".cc", + "../../ui/windowed_app_main_"..platform_suffix..".cc", }) -- Only create the .user file if it doesn't already exist. local user_file = project_root.."/build/xenia-gpu-d3d12-trace-viewer.vcxproj.user" @@ -101,7 +101,7 @@ project("xenia-gpu-d3d12-trace-dump") }) files({ "d3d12_trace_dump_main.cc", - "../../base/main_"..platform_suffix..".cc", + "../../base/console_app_main_"..platform_suffix..".cc", }) -- Only create the .user file if it doesn't already exist. local user_file = project_root.."/build/xenia-gpu-d3d12-trace-dump.vcxproj.user" diff --git a/src/xenia/gpu/graphics_system.cc b/src/xenia/gpu/graphics_system.cc index 4113212fb..6ffab9150 100644 --- a/src/xenia/gpu/graphics_system.cc +++ b/src/xenia/gpu/graphics_system.cc @@ -18,7 +18,8 @@ #include "xenia/gpu/command_processor.h" #include "xenia/gpu/gpu_flags.h" #include "xenia/ui/graphics_provider.h" -#include "xenia/ui/loop.h" +#include "xenia/ui/window.h" +#include "xenia/ui/windowed_app_context.h" DEFINE_bool( store_shaders, true, @@ -57,22 +58,24 @@ X_STATUS GraphicsSystem::Setup(cpu::Processor* processor, // This must happen on the UI thread. std::unique_ptr processor_context = nullptr; if (provider_) { - if (target_window_) { - target_window_->loop()->PostSynchronous([&]() { - // Create the context used for presentation. - assert_null(target_window->context()); - target_window_->set_context(provider_->CreateContext(target_window_)); - - // Setup the context the command processor will do all its drawing in. - // It's shared with the display context so that we can resolve - // framebuffers from it. - processor_context = provider()->CreateOffscreenContext(); - }); + // Setup the context the command processor will do all its drawing in. + bool contexts_initialized = true; + processor_context = provider()->CreateOffscreenContext(); + if (processor_context) { + if (target_window_) { + if (!target_window_->app_context().CallInUIThreadSynchronous([&]() { + // Create the context used for presentation. + assert_null(target_window->context()); + target_window_->set_context( + provider_->CreateContext(target_window_)); + })) { + contexts_initialized = false; + } + } } else { - processor_context = provider()->CreateOffscreenContext(); + contexts_initialized = false; } - - if (!processor_context) { + if (!contexts_initialized) { xe::FatalError( "Unable to initialize graphics context. Xenia requires Vulkan " "support.\n" diff --git a/src/xenia/gpu/null/null_graphics_system.cc b/src/xenia/gpu/null/null_graphics_system.cc index f1a88010f..c6df3ff09 100644 --- a/src/xenia/gpu/null/null_graphics_system.cc +++ b/src/xenia/gpu/null/null_graphics_system.cc @@ -26,7 +26,7 @@ X_STATUS NullGraphicsSystem::Setup(cpu::Processor* processor, ui::Window* target_window) { // This is a null graphics system, but we still setup vulkan because UI needs // it through us :| - provider_ = xe::ui::vulkan::VulkanProvider::Create(target_window); + provider_ = xe::ui::vulkan::VulkanProvider::Create(); return GraphicsSystem::Setup(processor, kernel_state, target_window); } diff --git a/src/xenia/gpu/premake5.lua b/src/xenia/gpu/premake5.lua index 87ccfc494..27e817f44 100644 --- a/src/xenia/gpu/premake5.lua +++ b/src/xenia/gpu/premake5.lua @@ -45,7 +45,7 @@ project("xenia-gpu-shader-compiler") }) files({ "shader_compiler_main.cc", - "../base/main_"..platform_suffix..".cc", + "../base/console_app_main_"..platform_suffix..".cc", }) filter("platforms:Windows") diff --git a/src/xenia/gpu/shader_compiler_main.cc b/src/xenia/gpu/shader_compiler_main.cc index 2ab319533..a3f9a7110 100644 --- a/src/xenia/gpu/shader_compiler_main.cc +++ b/src/xenia/gpu/shader_compiler_main.cc @@ -13,9 +13,9 @@ #include #include "xenia/base/assert.h" +#include "xenia/base/console_app_main.h" #include "xenia/base/cvar.h" #include "xenia/base/logging.h" -#include "xenia/base/main.h" #include "xenia/base/platform.h" #include "xenia/base/string.h" #include "xenia/base/string_buffer.h" @@ -229,5 +229,6 @@ int shader_compiler_main(const std::vector& args) { } // namespace gpu } // namespace xe -DEFINE_ENTRY_POINT("xenia-gpu-shader-compiler", xe::gpu::shader_compiler_main, - "shader.bin", "shader_input"); +XE_DEFINE_CONSOLE_APP("xenia-gpu-shader-compiler", + xe::gpu::shader_compiler_main, "shader.bin", + "shader_input"); diff --git a/src/xenia/gpu/trace_dump.cc b/src/xenia/gpu/trace_dump.cc index fdebcfba4..a78964d14 100644 --- a/src/xenia/gpu/trace_dump.cc +++ b/src/xenia/gpu/trace_dump.cc @@ -100,7 +100,7 @@ bool TraceDump::Setup() { return false; } graphics_system_ = emulator_->graphics_system(); - player_ = std::make_unique(nullptr, graphics_system_); + player_ = std::make_unique(graphics_system_); return true; } diff --git a/src/xenia/gpu/trace_player.cc b/src/xenia/gpu/trace_player.cc index 6152bafc5..8f2c98b8a 100644 --- a/src/xenia/gpu/trace_player.cc +++ b/src/xenia/gpu/trace_player.cc @@ -17,9 +17,8 @@ namespace xe { namespace gpu { -TracePlayer::TracePlayer(xe::ui::Loop* loop, GraphicsSystem* graphics_system) - : loop_(loop), - graphics_system_(graphics_system), +TracePlayer::TracePlayer(GraphicsSystem* graphics_system) + : graphics_system_(graphics_system), current_frame_index_(0), current_command_index_(-1) { // Need to allocate all of physical memory so that we can write to it during diff --git a/src/xenia/gpu/trace_player.h b/src/xenia/gpu/trace_player.h index 897faaff7..d56205d59 100644 --- a/src/xenia/gpu/trace_player.h +++ b/src/xenia/gpu/trace_player.h @@ -16,7 +16,6 @@ #include "xenia/base/threading.h" #include "xenia/gpu/trace_protocol.h" #include "xenia/gpu/trace_reader.h" -#include "xenia/ui/loop.h" namespace xe { namespace gpu { @@ -30,7 +29,7 @@ enum class TracePlaybackMode { class TracePlayer : public TraceReader { public: - TracePlayer(xe::ui::Loop* loop, GraphicsSystem* graphics_system); + TracePlayer(GraphicsSystem* graphics_system); ~TracePlayer() override; GraphicsSystem* graphics_system() const { return graphics_system_; } @@ -54,7 +53,6 @@ class TracePlayer : public TraceReader { void PlayTraceOnThread(const uint8_t* trace_data, size_t trace_size, TracePlaybackMode playback_mode, bool clear_caches); - xe::ui::Loop* loop_; GraphicsSystem* graphics_system_; int current_frame_index_; int current_command_index_; diff --git a/src/xenia/gpu/trace_viewer.cc b/src/xenia/gpu/trace_viewer.cc index cfc82bf2a..d5f389276 100644 --- a/src/xenia/gpu/trace_viewer.cc +++ b/src/xenia/gpu/trace_viewer.cc @@ -2,7 +2,7 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2020 Ben Vanik. All rights reserved. * + * Copyright 2021 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ @@ -13,10 +13,12 @@ #include "third_party/half/include/half.hpp" #include "third_party/imgui/imgui.h" +#include "xenia/base/assert.h" #include "xenia/base/clock.h" #include "xenia/base/logging.h" #include "xenia/base/math.h" #include "xenia/base/string.h" +#include "xenia/base/system.h" #include "xenia/base/threading.h" #include "xenia/gpu/command_processor.h" #include "xenia/gpu/gpu_flags.h" @@ -30,6 +32,7 @@ #include "xenia/ui/imgui_drawer.h" #include "xenia/ui/virtual_key.h" #include "xenia/ui/window.h" +#include "xenia/ui/windowed_app_context.h" #include "xenia/xbox.h" DEFINE_path(target_trace_file, "", "Specifies the trace file to load.", "GPU"); @@ -46,22 +49,16 @@ static const ImVec4 kColorComment = static const ImVec4 kColorIgnored = ImVec4(100 / 255.0f, 100 / 255.0f, 100 / 255.0f, 255 / 255.0f); -TraceViewer::TraceViewer() = default; +TraceViewer::TraceViewer(xe::ui::WindowedAppContext& app_context, + const std::string_view name) + : xe::ui::WindowedApp(app_context, name, "some.trace") { + AddPositionalOption("target_trace_file"); +} TraceViewer::~TraceViewer() = default; -int TraceViewer::Main(const std::vector& args) { - // Grab path from the flag or unnamed argument. - std::filesystem::path path; - if (!cvars::target_trace_file.empty()) { - // Passed as a named argument. - // TODO(benvanik): find something better than gflags that supports - // unicode. - path = cvars::target_trace_file; - } else if (args.size() >= 2) { - // Passed as an unnamed argument. - path = xe::to_path(args[1]); - } +bool TraceViewer::OnInitialize() { + std::filesystem::path path = cvars::target_trace_file; // If no path passed, ask the user. if (path.empty()) { @@ -83,42 +80,37 @@ int TraceViewer::Main(const std::vector& args) { } if (path.empty()) { - xe::FatalError("No trace file specified"); - return 1; + xe::ShowSimpleMessageBox(xe::SimpleMessageBoxType::Warning, + "No trace file specified"); + return false; } // Normalize the path and make absolute. auto abs_path = std::filesystem::absolute(path); if (!Setup()) { - xe::FatalError("Unable to setup trace viewer"); - return 1; + xe::ShowSimpleMessageBox(xe::SimpleMessageBoxType::Error, + "Unable to setup trace viewer"); + return false; } if (!Load(std::move(abs_path))) { - xe::FatalError("Unable to load trace file; not found?"); - return 1; + xe::ShowSimpleMessageBox(xe::SimpleMessageBoxType::Error, + "Unable to load trace file; not found?"); + return false; } - Run(); - return 0; + return true; } bool TraceViewer::Setup() { // Main display window. - loop_ = ui::Loop::Create(); - window_ = xe::ui::Window::Create(loop_.get(), "xenia-gpu-trace-viewer"); - loop_->PostSynchronous([&]() { - xe::threading::set_name("Win32 Loop"); - if (!window_->Initialize()) { - xe::FatalError("Failed to initialize main window"); - return; - } - }); - window_->on_closed.AddListener([&](xe::ui::UIEvent* e) { - loop_->Quit(); - XELOGI("User-initiated death!"); - exit(1); - }); - loop_->on_quit.AddListener([&](xe::ui::UIEvent* e) { window_.reset(); }); + assert_true(app_context().IsInUIThread()); + window_ = xe::ui::Window::Create(app_context(), "xenia-gpu-trace-viewer"); + if (!window_->Initialize()) { + XELOGE("Failed to initialize main window"); + return false; + } + window_->on_closed.AddListener( + [this](xe::ui::UIEvent* e) { app_context().QuitFromUIThread(); }); window_->Resize(1920, 1200); // Create the emulator but don't initialize so we can setup the window. @@ -142,9 +134,9 @@ bool TraceViewer::Setup() { } }); - player_ = std::make_unique(loop_.get(), graphics_system_); + player_ = std::make_unique(graphics_system_); - window_->on_painting.AddListener([&](xe::ui::UIEvent* e) { + window_->on_painting.AddListener([this](xe::ui::UIEvent* e) { DrawUI(); // Continuous paint. @@ -167,16 +159,6 @@ bool TraceViewer::Load(const std::filesystem::path& trace_file_path) { return true; } -void TraceViewer::Run() { - // Wait until we are exited. - loop_->AwaitQuit(); - - player_.reset(); - emulator_.reset(); - window_.reset(); - loop_.reset(); -} - void TraceViewer::DrawMultilineString(const std::string_view str) { size_t i = 0; bool done = false; diff --git a/src/xenia/gpu/trace_viewer.h b/src/xenia/gpu/trace_viewer.h index e7477c284..1e22439fb 100644 --- a/src/xenia/gpu/trace_viewer.h +++ b/src/xenia/gpu/trace_viewer.h @@ -2,7 +2,7 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2020 Ben Vanik. All rights reserved. * + * Copyright 2021 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ @@ -18,13 +18,8 @@ #include "xenia/gpu/trace_protocol.h" #include "xenia/gpu/xenos.h" #include "xenia/memory.h" - -namespace xe { -namespace ui { -class Loop; -class Window; -} // namespace ui -} // namespace xe +#include "xenia/ui/window.h" +#include "xenia/ui/windowed_app.h" namespace xe { namespace gpu { @@ -32,14 +27,15 @@ namespace gpu { struct SamplerInfo; struct TextureInfo; -class TraceViewer { +class TraceViewer : public xe::ui::WindowedApp { public: virtual ~TraceViewer(); - int Main(const std::vector& args); + bool OnInitialize() override; protected: - TraceViewer(); + explicit TraceViewer(xe::ui::WindowedAppContext& app_context, + const std::string_view name); virtual std::unique_ptr CreateGraphicsSystem() = 0; @@ -60,7 +56,6 @@ class TraceViewer { virtual bool Setup(); - std::unique_ptr loop_; std::unique_ptr window_; std::unique_ptr emulator_; Memory* memory_ = nullptr; @@ -75,7 +70,6 @@ class TraceViewer { }; bool Load(const std::filesystem::path& trace_file_path); - void Run(); void DrawUI(); void DrawControllerUI(); diff --git a/src/xenia/gpu/vulkan/premake5.lua b/src/xenia/gpu/vulkan/premake5.lua index 7db3b21f8..a8afadd5b 100644 --- a/src/xenia/gpu/vulkan/premake5.lua +++ b/src/xenia/gpu/vulkan/premake5.lua @@ -62,7 +62,7 @@ project("xenia-gpu-vulkan-trace-viewer") }) files({ "vulkan_trace_viewer_main.cc", - "../../base/main_"..platform_suffix..".cc", + "../../ui/windowed_app_main_"..platform_suffix..".cc", }) filter("platforms:Linux") @@ -128,7 +128,7 @@ project("xenia-gpu-vulkan-trace-dump") }) files({ "vulkan_trace_dump_main.cc", - "../../base/main_"..platform_suffix..".cc", + "../../base/console_app_main_"..platform_suffix..".cc", }) filter("platforms:Linux") diff --git a/src/xenia/gpu/vulkan/vulkan_graphics_system.cc b/src/xenia/gpu/vulkan/vulkan_graphics_system.cc index f789e531d..9c852c706 100644 --- a/src/xenia/gpu/vulkan/vulkan_graphics_system.cc +++ b/src/xenia/gpu/vulkan/vulkan_graphics_system.cc @@ -37,7 +37,7 @@ X_STATUS VulkanGraphicsSystem::Setup(cpu::Processor* processor, kernel::KernelState* kernel_state, ui::Window* target_window) { // Must create the provider so we can create contexts. - auto provider = xe::ui::vulkan::VulkanProvider::Create(target_window); + auto provider = xe::ui::vulkan::VulkanProvider::Create(); device_ = provider->device(); provider_ = std::move(provider); diff --git a/src/xenia/gpu/vulkan/vulkan_trace_dump_main.cc b/src/xenia/gpu/vulkan/vulkan_trace_dump_main.cc index 16c1f34b8..d1d78047c 100644 --- a/src/xenia/gpu/vulkan/vulkan_trace_dump_main.cc +++ b/src/xenia/gpu/vulkan/vulkan_trace_dump_main.cc @@ -7,8 +7,8 @@ ****************************************************************************** */ +#include "xenia/base/console_app_main.h" #include "xenia/base/logging.h" -#include "xenia/base/main.h" #include "xenia/gpu/trace_dump.h" #include "xenia/gpu/vulkan/vulkan_command_processor.h" #include "xenia/gpu/vulkan/vulkan_graphics_system.h" @@ -55,6 +55,6 @@ int trace_dump_main(const std::vector& args) { } // namespace gpu } // namespace xe -DEFINE_ENTRY_POINT("xenia-gpu-vulkan-trace-dump", - xe::gpu::vulkan::trace_dump_main, "some.trace", - "target_trace_file"); +XE_DEFINE_CONSOLE_APP("xenia-gpu-vulkan-trace-dump", + xe::gpu::vulkan::trace_dump_main, "some.trace", + "target_trace_file"); diff --git a/src/xenia/gpu/vulkan/vulkan_trace_viewer_main.cc b/src/xenia/gpu/vulkan/vulkan_trace_viewer_main.cc index 769a1b8c8..ed296d4e0 100644 --- a/src/xenia/gpu/vulkan/vulkan_trace_viewer_main.cc +++ b/src/xenia/gpu/vulkan/vulkan_trace_viewer_main.cc @@ -2,13 +2,15 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2020 Ben Vanik. All rights reserved. * + * Copyright 2021 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ +#include +#include + #include "xenia/base/logging.h" -#include "xenia/base/main.h" #include "xenia/gpu/trace_viewer.h" #include "xenia/gpu/vulkan/vulkan_command_processor.h" #include "xenia/gpu/vulkan/vulkan_graphics_system.h" @@ -19,8 +21,13 @@ namespace vulkan { using namespace xe::gpu::xenos; -class VulkanTraceViewer : public TraceViewer { +class VulkanTraceViewer final : public TraceViewer { public: + static std::unique_ptr Create( + xe::ui::WindowedAppContext& app_context) { + return std::unique_ptr(new VulkanTraceViewer(app_context)); + } + std::unique_ptr CreateGraphicsSystem() override { return std::unique_ptr(new VulkanGraphicsSystem()); } @@ -60,17 +67,15 @@ class VulkanTraceViewer : public TraceViewer { // return static_cast(texture->handle); return 0; } -}; -int trace_viewer_main(const std::vector& args) { - VulkanTraceViewer trace_viewer; - return trace_viewer.Main(args); -} + private: + explicit VulkanTraceViewer(xe::ui::WindowedAppContext& app_context) + : TraceViewer(app_context, "xenia-gpu-vulkan-trace-viewer") {} +}; } // namespace vulkan } // namespace gpu } // namespace xe -DEFINE_ENTRY_POINT("xenia-gpu-vulkan-trace-viewer", - xe::gpu::vulkan::trace_viewer_main, "some.trace", - "target_trace_file"); +XE_DEFINE_WINDOWED_APP(xenia_gpu_vulkan_trace_viewer, + xe::gpu::vulkan::VulkanTraceViewer::Create); diff --git a/src/xenia/hid/hid_demo.cc b/src/xenia/hid/hid_demo.cc index c1a81f533..d02735a42 100644 --- a/src/xenia/hid/hid_demo.cc +++ b/src/xenia/hid/hid_demo.cc @@ -10,15 +10,17 @@ #include #include #include +#include +#include #include #include +#include #include "third_party/fmt/include/fmt/format.h" #include "third_party/imgui/imgui.h" #include "xenia/base/clock.h" #include "xenia/base/cvar.h" #include "xenia/base/logging.h" -#include "xenia/base/main.h" #include "xenia/base/threading.h" #include "xenia/hid/hid_flags.h" #include "xenia/hid/input_system.h" @@ -26,6 +28,7 @@ #include "xenia/ui/virtual_key.h" #include "xenia/ui/vulkan/vulkan_provider.h" #include "xenia/ui/window.h" +#include "xenia/ui/windowed_app.h" // Available input drivers: #include "xenia/hid/nop/nop_hid.h" @@ -46,10 +49,36 @@ DEFINE_string(hid, "any", "Input system. Use: [any, nop, sdl, winkey, xinput]", namespace xe { namespace hid { -std::unique_ptr input_system_; -bool is_active = true; +class HidDemoApp final : public ui::WindowedApp { + public: + static std::unique_ptr Create( + ui::WindowedAppContext& app_context) { + return std::unique_ptr(new HidDemoApp(app_context)); + } -std::vector> CreateInputDrivers( + bool OnInitialize() override; + + private: + explicit HidDemoApp(ui::WindowedAppContext& app_context) + : ui::WindowedApp(app_context, "xenia-hid-demo") {} + + static std::vector> CreateInputDrivers( + ui::Window* window); + + void DrawUserInputGetState(uint32_t user_index) const; + void DrawInputGetState() const; + void DrawUserInputGetKeystroke(uint32_t user_index, bool poll, + bool hide_repeats, bool clear_log) const; + void DrawInputGetKeystroke(bool poll, bool hide_repeats, + bool clear_log) const; + + std::unique_ptr graphics_provider_; + std::unique_ptr window_; + std::unique_ptr input_system_; + bool is_active_ = true; +}; + +std::vector> HidDemoApp::CreateInputDrivers( ui::Window* window) { std::vector> drivers; if (cvars::hid.compare("nop") == 0) { @@ -94,60 +123,46 @@ std::vector> CreateInputDrivers( return drivers; } -std::unique_ptr CreateDemoGraphicsProvider( - xe::ui::Window* window) { - return xe::ui::vulkan::VulkanProvider::Create(window); -} +bool HidDemoApp::OnInitialize() { + // Create graphics provider that provides the context for the window. + graphics_provider_ = xe::ui::vulkan::VulkanProvider::Create(); + if (!graphics_provider_) { + return false; + } -void DrawInputGetState(); -void DrawInputGetKeystroke(bool poll, bool hide_repeats, bool clear_log); - -int hid_demo_main(const std::vector& args) { - // Create run loop and the window. - auto loop = ui::Loop::Create(); - auto window = xe::ui::Window::Create(loop.get(), GetEntryInfo().name); - loop->PostSynchronous([&window]() { - xe::threading::set_name("Win32 Loop"); - if (!window->Initialize()) { - FatalError("Failed to initialize main window"); - return; - } - }); - window->on_closed.AddListener([&loop](xe::ui::UIEvent* e) { - loop->Quit(); + // Create the window. + window_ = xe::ui::Window::Create(app_context(), GetName()); + if (!window_->Initialize()) { + XELOGE("Failed to initialize main window"); + return false; + } + window_->on_closed.AddListener([this](xe::ui::UIEvent* e) { XELOGI("User-initiated death!"); - exit(1); + app_context().QuitFromUIThread(); }); - loop->on_quit.AddListener([&window](xe::ui::UIEvent* e) { window.reset(); }); // Initial size setting, done here so that it knows the menu exists. - window->Resize(COL_WIDTH_STATE + COL_WIDTH_STROKE, ROW_HEIGHT_GENERAL + 500); + window_->Resize(COL_WIDTH_STATE + COL_WIDTH_STROKE, ROW_HEIGHT_GENERAL + 500); - // Create the graphics context used for drawing and setup the window. - std::unique_ptr graphics_provider; - loop->PostSynchronous([&window, &graphics_provider]() { - // Create context and give it to the window. - // The window will finish initialization wtih the context (loading - // resources, etc). - graphics_provider = CreateDemoGraphicsProvider(window.get()); - window->set_context(graphics_provider->CreateContext(window.get())); + // Create the graphics context for the window. The window will finish + // initialization with the context (loading resources, etc). + window_->set_context(graphics_provider_->CreateContext(window_.get())); - // Initialize input system and all drivers. - input_system_ = std::make_unique(window.get()); - auto drivers = CreateInputDrivers(window.get()); - for (size_t i = 0; i < drivers.size(); ++i) { - auto& driver = drivers[i]; - driver->set_is_active_callback([]() -> bool { return is_active; }); - input_system_->AddDriver(std::move(driver)); - } + // Initialize input system and all drivers. + input_system_ = std::make_unique(window_.get()); + auto drivers = CreateInputDrivers(window_.get()); + for (size_t i = 0; i < drivers.size(); ++i) { + auto& driver = drivers[i]; + driver->set_is_active_callback([this]() -> bool { return is_active_; }); + input_system_->AddDriver(std::move(driver)); + } - window->Invalidate(); - }); + window_->Invalidate(); - window->set_imgui_input_enabled(true); + window_->set_imgui_input_enabled(true); - window->on_painting.AddListener([&](xe::ui::UIEvent* e) { - auto& io = window->imgui_drawer()->GetIO(); + window_->on_painting.AddListener([this](xe::ui::UIEvent* e) { + auto& io = window_->imgui_drawer()->GetIO(); const ImGuiWindowFlags wflags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | @@ -161,7 +176,7 @@ int hid_demo_main(const std::vector& args) { ImVec2(COL_WIDTH_STATE + COL_WIDTH_STROKE, ROW_HEIGHT_GENERAL)); ImGui::Text("Input System (hid) = \"%s\"", cvars::hid.c_str()); - ImGui::Checkbox("is_active", &is_active); + ImGui::Checkbox("is_active", &is_active_); } ImGui::End(); @@ -201,21 +216,13 @@ int hid_demo_main(const std::vector& args) { ImGui::End(); // Continuous paint. - window->Invalidate(); + window_->Invalidate(); }); - // Wait until we are exited. - loop->AwaitQuit(); - - input_system_.reset(); - - loop->PostSynchronous([&graphics_provider]() { graphics_provider.reset(); }); - window.reset(); - loop.reset(); - return 0; + return true; } -void DrawUserInputGetState(uint32_t user_index) { +void HidDemoApp::DrawUserInputGetState(uint32_t user_index) const { ImGui::Text("User %u:", user_index); X_INPUT_STATE state; @@ -274,7 +281,7 @@ void DrawUserInputGetState(uint32_t user_index) { ImGui::Text(" "); } -void DrawInputGetState() { +void HidDemoApp::DrawInputGetState() const { ImGui::BeginChild("##input_get_state_scroll"); for (uint32_t user_index = 0; user_index < MAX_USERS; ++user_index) { DrawUserInputGetState(user_index); @@ -282,46 +289,48 @@ void DrawInputGetState() { ImGui::EndChild(); } -static const std::unordered_map kVkPretty = { - {ui::VirtualKey::kXInputPadA, "A"}, - {ui::VirtualKey::kXInputPadB, "B"}, - {ui::VirtualKey::kXInputPadX, "X"}, - {ui::VirtualKey::kXInputPadY, "Y"}, - {ui::VirtualKey::kXInputPadRShoulder, "R Shoulder"}, - {ui::VirtualKey::kXInputPadLShoulder, "L Shoulder"}, - {ui::VirtualKey::kXInputPadLTrigger, "L Trigger"}, - {ui::VirtualKey::kXInputPadRTrigger, "R Trigger"}, +void HidDemoApp::DrawUserInputGetKeystroke(uint32_t user_index, bool poll, + bool hide_repeats, + bool clear_log) const { + static const std::unordered_map kVkPretty = + { + {ui::VirtualKey::kXInputPadA, "A"}, + {ui::VirtualKey::kXInputPadB, "B"}, + {ui::VirtualKey::kXInputPadX, "X"}, + {ui::VirtualKey::kXInputPadY, "Y"}, + {ui::VirtualKey::kXInputPadRShoulder, "R Shoulder"}, + {ui::VirtualKey::kXInputPadLShoulder, "L Shoulder"}, + {ui::VirtualKey::kXInputPadLTrigger, "L Trigger"}, + {ui::VirtualKey::kXInputPadRTrigger, "R Trigger"}, - {ui::VirtualKey::kXInputPadDpadUp, "DPad up"}, - {ui::VirtualKey::kXInputPadDpadDown, "DPad down"}, - {ui::VirtualKey::kXInputPadDpadLeft, "DPad left"}, - {ui::VirtualKey::kXInputPadDpadRight, "DPad right"}, - {ui::VirtualKey::kXInputPadStart, "Start"}, - {ui::VirtualKey::kXInputPadBack, "Back"}, - {ui::VirtualKey::kXInputPadLThumbPress, "L Thumb press"}, - {ui::VirtualKey::kXInputPadRThumbPress, "R Thumb press"}, + {ui::VirtualKey::kXInputPadDpadUp, "DPad up"}, + {ui::VirtualKey::kXInputPadDpadDown, "DPad down"}, + {ui::VirtualKey::kXInputPadDpadLeft, "DPad left"}, + {ui::VirtualKey::kXInputPadDpadRight, "DPad right"}, + {ui::VirtualKey::kXInputPadStart, "Start"}, + {ui::VirtualKey::kXInputPadBack, "Back"}, + {ui::VirtualKey::kXInputPadLThumbPress, "L Thumb press"}, + {ui::VirtualKey::kXInputPadRThumbPress, "R Thumb press"}, - {ui::VirtualKey::kXInputPadLThumbUp, "L Thumb up"}, - {ui::VirtualKey::kXInputPadLThumbDown, "L Thumb down"}, - {ui::VirtualKey::kXInputPadLThumbRight, "L Thumb right"}, - {ui::VirtualKey::kXInputPadLThumbLeft, "L Thumb left"}, - {ui::VirtualKey::kXInputPadLThumbUpLeft, "L Thumb up & left"}, - {ui::VirtualKey::kXInputPadLThumbUpRight, "L Thumb up & right"}, - {ui::VirtualKey::kXInputPadLThumbDownRight, "L Thumb down & right"}, - {ui::VirtualKey::kXInputPadLThumbDownLeft, "L Thumb down & left"}, + {ui::VirtualKey::kXInputPadLThumbUp, "L Thumb up"}, + {ui::VirtualKey::kXInputPadLThumbDown, "L Thumb down"}, + {ui::VirtualKey::kXInputPadLThumbRight, "L Thumb right"}, + {ui::VirtualKey::kXInputPadLThumbLeft, "L Thumb left"}, + {ui::VirtualKey::kXInputPadLThumbUpLeft, "L Thumb up & left"}, + {ui::VirtualKey::kXInputPadLThumbUpRight, "L Thumb up & right"}, + {ui::VirtualKey::kXInputPadLThumbDownRight, "L Thumb down & right"}, + {ui::VirtualKey::kXInputPadLThumbDownLeft, "L Thumb down & left"}, - {ui::VirtualKey::kXInputPadRThumbUp, "R Thumb up"}, - {ui::VirtualKey::kXInputPadRThumbDown, "R Thumb down"}, - {ui::VirtualKey::kXInputPadRThumbRight, "R Thumb right"}, - {ui::VirtualKey::kXInputPadRThumbLeft, "R Thumb left"}, - {ui::VirtualKey::kXInputPadRThumbUpLeft, "R Thumb up & left"}, - {ui::VirtualKey::kXInputPadRThumbUpRight, "R Thumb up & right"}, - {ui::VirtualKey::kXInputPadRThumbDownRight, "R Thumb down & right"}, - {ui::VirtualKey::kXInputPadRThumbDownLeft, "R Thumb down & left"}, -}; + {ui::VirtualKey::kXInputPadRThumbUp, "R Thumb up"}, + {ui::VirtualKey::kXInputPadRThumbDown, "R Thumb down"}, + {ui::VirtualKey::kXInputPadRThumbRight, "R Thumb right"}, + {ui::VirtualKey::kXInputPadRThumbLeft, "R Thumb left"}, + {ui::VirtualKey::kXInputPadRThumbUpLeft, "R Thumb up & left"}, + {ui::VirtualKey::kXInputPadRThumbUpRight, "R Thumb up & right"}, + {ui::VirtualKey::kXInputPadRThumbDownRight, "R Thumb down & right"}, + {ui::VirtualKey::kXInputPadRThumbDownLeft, "R Thumb down & left"}, + }; -void DrawUserInputGetKeystroke(uint32_t user_index, bool poll, - bool hide_repeats, bool clear_log) { const size_t maxLog = 128; static std::array, MAX_USERS> event_logs; static std::array last_event_times = {}; @@ -400,7 +409,8 @@ void DrawUserInputGetKeystroke(uint32_t user_index, bool poll, } } -void DrawInputGetKeystroke(bool poll, bool hide_repeats, bool clear_log) { +void HidDemoApp::DrawInputGetKeystroke(bool poll, bool hide_repeats, + bool clear_log) const { bool tab_bar = ImGui::BeginTabBar("DrawInputGetKeystroke"); for (uint32_t user_index = 0; user_index < MAX_USERS; ++user_index) { DrawUserInputGetKeystroke(user_index, poll, hide_repeats, clear_log); @@ -411,4 +421,4 @@ void DrawInputGetKeystroke(bool poll, bool hide_repeats, bool clear_log) { } // namespace hid } // namespace xe -DEFINE_ENTRY_POINT("xenia-hid-demo", xe::hid::hid_demo_main, ""); +XE_DEFINE_WINDOWED_APP(xenia_hid_demo, xe::hid::HidDemoApp::Create); diff --git a/src/xenia/hid/premake5.lua b/src/xenia/hid/premake5.lua index ce89331c9..1aeef5657 100644 --- a/src/xenia/hid/premake5.lua +++ b/src/xenia/hid/premake5.lua @@ -32,7 +32,7 @@ project("xenia-hid-demo") }) files({ "hid_demo.cc", - "../base/main_"..platform_suffix..".cc", + "../ui/windowed_app_main_"..platform_suffix..".cc", }) resincludedirs({ project_root, diff --git a/src/xenia/hid/sdl/sdl_input_driver.cc b/src/xenia/hid/sdl/sdl_input_driver.cc index 0e4e7fd11..6b348f285 100644 --- a/src/xenia/hid/sdl/sdl_input_driver.cc +++ b/src/xenia/hid/sdl/sdl_input_driver.cc @@ -22,6 +22,7 @@ #include "xenia/hid/hid_flags.h" #include "xenia/ui/virtual_key.h" #include "xenia/ui/window.h" +#include "xenia/ui/windowed_app_context.h" // TODO(joellinn) make this path relative to the config folder. DEFINE_path(mappings_file, "gamecontrollerdb.txt", @@ -43,6 +44,12 @@ SDLInputDriver::SDLInputDriver(xe::ui::Window* window) keystroke_states_() {} SDLInputDriver::~SDLInputDriver() { + // Make sure the CallInUIThread is executed before destroying the references. + if (sdl_pumpevents_queued_) { + window()->app_context().CallInUIThreadSynchronous([this]() { + window()->app_context().ExecutePendingFunctionsFromUIThread(); + }); + } for (size_t i = 0; i < controllers_.size(); i++) { if (controllers_.at(i).sdl) { SDL_GameControllerClose(controllers_.at(i).sdl); @@ -65,8 +72,9 @@ X_STATUS SDLInputDriver::Setup() { } // SDL_PumpEvents should only be run in the thread that initialized SDL - we - // are hijacking the window loop thread for that. - window()->loop()->PostSynchronous([&]() { + // are hijacking the UI thread for that. If this function fails to be queued, + // the "initialized" variables will be false - that's handled safely. + window()->app_context().CallInUIThreadSynchronous([this]() { if (!xe::helper::sdl::SDLHelper::Prepare()) { return; } @@ -129,7 +137,9 @@ X_STATUS SDLInputDriver::Setup() { } } }); - return sdl_events_initialized_ && sdl_gamecontroller_initialized_; + return (sdl_events_initialized_ && sdl_gamecontroller_initialized_) + ? X_STATUS_SUCCESS + : X_STATUS_UNSUCCESSFUL; } X_RESULT SDLInputDriver::GetCapabilities(uint32_t user_index, uint32_t flags, @@ -693,7 +703,7 @@ void SDLInputDriver::QueueControllerUpdate() { bool is_queued = false; sdl_pumpevents_queued_.compare_exchange_strong(is_queued, true); if (!is_queued) { - window()->loop()->Post([this]() { + window()->app_context().CallInUIThread([this]() { SDL_PumpEvents(); sdl_pumpevents_queued_ = false; }); diff --git a/src/xenia/kernel/xam/xam_nui.cc b/src/xenia/kernel/xam/xam_nui.cc index 2fc1a2a52..d0f99087d 100644 --- a/src/xenia/kernel/xam/xam_nui.cc +++ b/src/xenia/kernel/xam/xam_nui.cc @@ -15,6 +15,7 @@ #include "xenia/kernel/xam/xam_private.h" #include "xenia/ui/imgui_dialog.h" #include "xenia/ui/window.h" +#include "xenia/ui/windowed_app_context.h" #include "xenia/xbox.h" namespace xe { @@ -50,15 +51,16 @@ dword_result_t XamShowNuiTroubleshooterUI(unknown_t unk1, unknown_t unk2, auto display_window = kernel_state()->emulator()->display_window(); xe::threading::Fence fence; - display_window->loop()->PostSynchronous([&]() { - xe::ui::ImGuiDialog::ShowMessageBox( - display_window, "NUI Troubleshooter", - "The game has indicated there is a problem with NUI (Kinect).") - ->Then(&fence); - }); - ++xam_dialogs_shown_; - fence.Wait(); - --xam_dialogs_shown_; + if (display_window->app_context().CallInUIThreadSynchronous([&]() { + xe::ui::ImGuiDialog::ShowMessageBox( + display_window, "NUI Troubleshooter", + "The game has indicated there is a problem with NUI (Kinect).") + ->Then(&fence); + })) { + ++xam_dialogs_shown_; + fence.Wait(); + --xam_dialogs_shown_; + } return 0; } diff --git a/src/xenia/kernel/xam/xam_ui.cc b/src/xenia/kernel/xam/xam_ui.cc index b88ffbdb5..21b0590d6 100644 --- a/src/xenia/kernel/xam/xam_ui.cc +++ b/src/xenia/kernel/xam/xam_ui.cc @@ -17,6 +17,7 @@ #include "xenia/kernel/xam/xam_private.h" #include "xenia/ui/imgui_dialog.h" #include "xenia/ui/window.h" +#include "xenia/ui/windowed_app_context.h" #include "xenia/xbox.h" namespace xe { @@ -76,11 +77,16 @@ X_RESULT xeXamDispatchDialog(T* dialog, result = close_callback(dialog); }); xe::threading::Fence fence; - kernel_state()->emulator()->display_window()->loop()->PostSynchronous( - [&dialog, &fence]() { dialog->Then(&fence); }); - ++xam_dialogs_shown_; - fence.Wait(); - --xam_dialogs_shown_; + xe::ui::WindowedAppContext& app_context = + kernel_state()->emulator()->display_window()->app_context(); + if (app_context.CallInUIThreadSynchronous( + [&dialog, &fence]() { dialog->Then(&fence); })) { + ++xam_dialogs_shown_; + fence.Wait(); + --xam_dialogs_shown_; + } else { + delete dialog; + } // dialog should be deleted at this point! return result; }; @@ -117,11 +123,14 @@ X_RESULT xeXamDispatchDialogEx( result = close_callback(dialog, extended_error, length); }); xe::threading::Fence fence; - display_window->loop()->PostSynchronous( - [&dialog, &fence]() { dialog->Then(&fence); }); - ++xam_dialogs_shown_; - fence.Wait(); - --xam_dialogs_shown_; + if (display_window->app_context().CallInUIThreadSynchronous( + [&dialog, &fence]() { dialog->Then(&fence); })) { + ++xam_dialogs_shown_; + fence.Wait(); + --xam_dialogs_shown_; + } else { + delete dialog; + } // dialog should be deleted at this point! return result; }; diff --git a/src/xenia/kernel/xthread.cc b/src/xenia/kernel/xthread.cc index e11a94296..43303c04e 100644 --- a/src/xenia/kernel/xthread.cc +++ b/src/xenia/kernel/xthread.cc @@ -11,10 +11,6 @@ #include -#ifdef XE_PLATFORM_WIN32 -#include -#endif - #include "third_party/fmt/include/fmt/format.h" #include "xenia/base/byte_stream.h" #include "xenia/base/clock.h" @@ -383,20 +379,6 @@ X_STATUS XThread::Create() { // Set name immediately, if we have one. thread_->set_name(thread_name_); -#ifdef XE_PLATFORM_WIN32 - // Setup COM on this thread. - // - // https://devblogs.microsoft.com/oldnewthing/?p=4613 - // - // "If any thread in the process calls CoInitialize[Ex] with the - // COINIT_MULTITHREADED flag, then that not only initializes the current - // thread as a member of the multi-threaded apartment, but it also says, - // "Any thread which has never called CoInitialize[Ex] is also part of the - // multi-threaded apartment." -#pragma warning(suppress : 6031) - CoInitializeEx(nullptr, COINIT_MULTITHREADED); -#endif - // Profiler needs to know about the thread. xe::Profiler::ThreadEnter(thread_name_.c_str()); diff --git a/src/xenia/tools/api-scanner/api_scanner_loader.h b/src/xenia/tools/api-scanner/api_scanner_loader.h index 9aa8d7fe9..d4438cd19 100644 --- a/src/xenia/tools/api-scanner/api_scanner_loader.h +++ b/src/xenia/tools/api-scanner/api_scanner_loader.h @@ -7,11 +7,12 @@ ****************************************************************************** */ +#include +#include +#include #include -#include "xenia/base/main.h" #include "xenia/base/math.h" -#include "xenia/base/string.h" #include "xenia/cpu/export_resolver.h" #include "xenia/kernel/fs/filesystem.h" #include "xenia/kernel/objects/xfile.h" diff --git a/src/xenia/tools/api-scanner/api_scanner_main.cc b/src/xenia/tools/api-scanner/api_scanner_main.cc index 738e9622d..bd9e1fae3 100644 --- a/src/xenia/tools/api-scanner/api_scanner_main.cc +++ b/src/xenia/tools/api-scanner/api_scanner_main.cc @@ -7,17 +7,25 @@ ****************************************************************************** */ -#include "api_scanner_loader.h" +#include +#include +#include + +#include "xenia/base/console_app_main.h" +#include "xenia/base/cvar.h" +#include "xenia/base/string.h" +#include "xenia/tools/api_scanner_loader.h" namespace xe { namespace tools { -DEFINE_string(target, "", "List of file to extract imports from"); +DEFINE_transient_string(target, "", "List of file to extract imports from"); +// TODO(Triang3l): Change to std::string (currently doesn't even compile). int api_scanner_main(std::vector& args) { // XXX we need gflags to split multiple flags into arrays for us - if (args.size() == 2 || !FLAGS_target.empty()) { + if (args.size() == 2 || !cvars::target.empty()) { apiscanner_loader loader_; std::wstring target(cvars::target.empty() ? args[1] : xe::to_wstring(cvars::target)); @@ -41,5 +49,5 @@ int api_scanner_main(std::vector& args) { } // namespace tools } // namespace xe -DEFINE_ENTRY_POINT(L"api-scanner", L"api-scanner --target=", - xe::tools::api_scanner_main); +XE_DEFINE_CONSOLE_APP("api-scanner", xe::tools::api_scanner_main, + "[target file]", "target"); diff --git a/src/xenia/ui/d3d12/d3d12_provider.cc b/src/xenia/ui/d3d12/d3d12_provider.cc index 9560cfa97..57345824d 100644 --- a/src/xenia/ui/d3d12/d3d12_provider.cc +++ b/src/xenia/ui/d3d12/d3d12_provider.cc @@ -47,8 +47,8 @@ bool D3D12Provider::IsD3D12APIAvailable() { return true; } -std::unique_ptr D3D12Provider::Create(Window* main_window) { - std::unique_ptr provider(new D3D12Provider(main_window)); +std::unique_ptr D3D12Provider::Create() { + std::unique_ptr provider(new D3D12Provider); if (!provider->Initialize()) { xe::FatalError( "Unable to initialize Direct3D 12 graphics subsystem.\n" @@ -63,9 +63,6 @@ std::unique_ptr D3D12Provider::Create(Window* main_window) { return provider; } -D3D12Provider::D3D12Provider(Window* main_window) - : GraphicsProvider(main_window) {} - D3D12Provider::~D3D12Provider() { if (graphics_analysis_ != nullptr) { graphics_analysis_->Release(); diff --git a/src/xenia/ui/d3d12/d3d12_provider.h b/src/xenia/ui/d3d12/d3d12_provider.h index 25c159dcd..3ddaf507d 100644 --- a/src/xenia/ui/d3d12/d3d12_provider.h +++ b/src/xenia/ui/d3d12/d3d12_provider.h @@ -27,7 +27,7 @@ class D3D12Provider : public GraphicsProvider { static bool IsD3D12APIAvailable(); - static std::unique_ptr Create(Window* main_window); + static std::unique_ptr Create(); std::unique_ptr CreateContext( Window* target_window) override; @@ -147,7 +147,7 @@ class D3D12Provider : public GraphicsProvider { } private: - explicit D3D12Provider(Window* main_window); + D3D12Provider() = default; static bool EnableIncreaseBasePriorityPrivilege(); bool Initialize(); diff --git a/src/xenia/ui/d3d12/d3d12_window_demo.cc b/src/xenia/ui/d3d12/d3d12_window_demo.cc index d42e6cb0e..93b65ac67 100644 --- a/src/xenia/ui/d3d12/d3d12_window_demo.cc +++ b/src/xenia/ui/d3d12/d3d12_window_demo.cc @@ -2,28 +2,44 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2020 Ben Vanik. All rights reserved. * + * Copyright 2021 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ +#include #include -#include -#include "xenia/base/main.h" #include "xenia/ui/d3d12/d3d12_provider.h" -#include "xenia/ui/window.h" +#include "xenia/ui/window_demo.h" +#include "xenia/ui/windowed_app.h" namespace xe { namespace ui { +namespace d3d12 { -int window_demo_main(const std::vector& args); +class D3D12WindowDemoApp final : public WindowDemoApp { + public: + static std::unique_ptr Create(WindowedAppContext& app_context) { + return std::unique_ptr(new D3D12WindowDemoApp(app_context)); + } -std::unique_ptr CreateDemoGraphicsProvider(Window* window) { - return xe::ui::d3d12::D3D12Provider::Create(window); + protected: + std::unique_ptr CreateGraphicsProvider() const override; + + private: + explicit D3D12WindowDemoApp(WindowedAppContext& app_context) + : WindowDemoApp(app_context, "xenia-ui-window-d3d12-demo") {} +}; + +std::unique_ptr D3D12WindowDemoApp::CreateGraphicsProvider() + const { + return D3D12Provider::Create(); } +} // namespace d3d12 } // namespace ui } // namespace xe -DEFINE_ENTRY_POINT("xenia-ui-window-d3d12-demo", xe::ui::window_demo_main, ""); +XE_DEFINE_WINDOWED_APP(xenia_ui_window_d3d12_demo, + xe::ui::d3d12::D3D12WindowDemoApp::Create); diff --git a/src/xenia/ui/d3d12/premake5.lua b/src/xenia/ui/d3d12/premake5.lua index 1615ee611..0e33011e5 100644 --- a/src/xenia/ui/d3d12/premake5.lua +++ b/src/xenia/ui/d3d12/premake5.lua @@ -30,7 +30,7 @@ project("xenia-ui-window-d3d12-demo") files({ "../window_demo.cc", "d3d12_window_demo.cc", - project_root.."/src/xenia/base/main_"..platform_suffix..".cc", + project_root.."/src/xenia/ui/windowed_app_main_"..platform_suffix..".cc", }) resincludedirs({ project_root, diff --git a/src/xenia/ui/file_picker_win.cc b/src/xenia/ui/file_picker_win.cc index 7af0fd57e..d2d7bf33d 100644 --- a/src/xenia/ui/file_picker_win.cc +++ b/src/xenia/ui/file_picker_win.cc @@ -12,6 +12,9 @@ #include "xenia/base/string.h" #include "xenia/ui/file_picker.h" +// Microsoft headers after platform_win.h. +#include + namespace xe { namespace ui { @@ -106,32 +109,16 @@ Win32FilePicker::Win32FilePicker() = default; Win32FilePicker::~Win32FilePicker() = default; -template -struct com_ptr { - com_ptr() : value(nullptr) {} - ~com_ptr() { reset(); } - void reset() { - if (value) { - value->Release(); - value = nullptr; - } - } - operator T*() { return value; } - T* operator->() const { return value; } - T** addressof() { return &value; } - T* value; -}; - bool Win32FilePicker::Show(void* parent_window_handle) { // TODO(benvanik): FileSaveDialog. assert_true(mode() == Mode::kOpen); // TODO(benvanik): folder dialogs. assert_true(type() == Type::kFile); - com_ptr file_dialog; + Microsoft::WRL::ComPtr file_dialog; HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(file_dialog.addressof())); + IID_PPV_ARGS(&file_dialog)); if (!SUCCEEDED(hr)) { return false; } @@ -180,14 +167,13 @@ bool Win32FilePicker::Show(void* parent_window_handle) { } // Create an event handling object, and hook it up to the dialog. - com_ptr file_dialog_events; - hr = CDialogEventHandler_CreateInstance( - IID_PPV_ARGS(file_dialog_events.addressof())); + Microsoft::WRL::ComPtr file_dialog_events; + hr = CDialogEventHandler_CreateInstance(IID_PPV_ARGS(&file_dialog_events)); if (!SUCCEEDED(hr)) { return false; } DWORD cookie; - hr = file_dialog->Advise(file_dialog_events, &cookie); + hr = file_dialog->Advise(file_dialog_events.Get(), &cookie); if (!SUCCEEDED(hr)) { return false; } @@ -201,8 +187,8 @@ bool Win32FilePicker::Show(void* parent_window_handle) { // Obtain the result once the user clicks the 'Open' button. // The result is an IShellItem object. - com_ptr shell_item; - hr = file_dialog->GetResult(shell_item.addressof()); + Microsoft::WRL::ComPtr shell_item; + hr = file_dialog->GetResult(&shell_item); if (!SUCCEEDED(hr)) { return false; } diff --git a/src/xenia/ui/graphics_provider.h b/src/xenia/ui/graphics_provider.h index ccb9ab771..8bbfe90d0 100644 --- a/src/xenia/ui/graphics_provider.h +++ b/src/xenia/ui/graphics_provider.h @@ -36,9 +36,6 @@ class GraphicsProvider { virtual ~GraphicsProvider() = default; - // The 'main' window of an application, used to query provider information. - Window* main_window() const { return main_window_; } - // Creates a new graphics context and swapchain for presenting to a window. virtual std::unique_ptr CreateContext( Window* target_window) = 0; @@ -48,9 +45,7 @@ class GraphicsProvider { virtual std::unique_ptr CreateOffscreenContext() = 0; protected: - explicit GraphicsProvider(Window* main_window) : main_window_(main_window) {} - - Window* main_window_ = nullptr; + GraphicsProvider() = default; }; } // namespace ui diff --git a/src/xenia/ui/loop.cc b/src/xenia/ui/loop.cc deleted file mode 100644 index 4fc6ad1dc..000000000 --- a/src/xenia/ui/loop.cc +++ /dev/null @@ -1,37 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2014 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#include "xenia/ui/loop.h" - -#include "xenia/base/assert.h" -#include "xenia/base/threading.h" - -namespace xe { -namespace ui { - -Loop::Loop() = default; - -Loop::~Loop() = default; - -void Loop::PostSynchronous(std::function fn) { - if (is_on_loop_thread()) { - // Prevent deadlock if we are executing on ourselves. - fn(); - return; - } - xe::threading::Fence fence; - Post([&fn, &fence]() { - fn(); - fence.Signal(); - }); - fence.Wait(); -} - -} // namespace ui -} // namespace xe diff --git a/src/xenia/ui/loop.h b/src/xenia/ui/loop.h deleted file mode 100644 index 06e49e461..000000000 --- a/src/xenia/ui/loop.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2014 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#ifndef XENIA_UI_LOOP_H_ -#define XENIA_UI_LOOP_H_ - -#include -#include - -#include "xenia/base/delegate.h" -#include "xenia/ui/ui_event.h" - -namespace xe { -namespace ui { - -class Loop { - public: - static std::unique_ptr Create(); - - Loop(); - virtual ~Loop(); - - // Returns true if the currently executing code is within the loop thread. - virtual bool is_on_loop_thread() = 0; - - virtual void Post(std::function fn) = 0; - virtual void PostDelayed(std::function fn, uint64_t delay_millis) = 0; - void PostSynchronous(std::function fn); - - virtual void Quit() = 0; - virtual void AwaitQuit() = 0; - - Delegate on_quit; -}; - -} // namespace ui -} // namespace xe - -#endif // XENIA_UI_LOOP_H_ diff --git a/src/xenia/ui/loop_gtk.cc b/src/xenia/ui/loop_gtk.cc deleted file mode 100644 index a2b704aeb..000000000 --- a/src/xenia/ui/loop_gtk.cc +++ /dev/null @@ -1,81 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2020 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#include "xenia/ui/loop_gtk.h" - -#include -#include - -#include "xenia/base/assert.h" - -namespace xe { -namespace ui { - -std::unique_ptr Loop::Create() { return std::make_unique(); } - -GTKLoop::GTKLoop() : thread_id_() { - gtk_init(nullptr, nullptr); - xe::threading::Fence init_fence; - thread_ = std::thread([&init_fence, this]() { - xe::threading::set_name("GTK Loop"); - - thread_id_ = std::this_thread::get_id(); - init_fence.Signal(); - - ThreadMain(); - - quit_fence_.Signal(); - }); - init_fence.Wait(); -} - -GTKLoop::~GTKLoop() { - Quit(); - thread_.join(); -} - -void GTKLoop::ThreadMain() { gtk_main(); } - -bool GTKLoop::is_on_loop_thread() { - return thread_id_ == std::this_thread::get_id(); -} - -gboolean _posted_fn_thunk(gpointer posted_fn) { - // convert gpointer back to std::function, call it, then free std::function - std::function* f = - reinterpret_cast*>(posted_fn); - std::function& func = *f; - func(); - delete f; - // Tells GDK we don't want to run this again - return G_SOURCE_REMOVE; -} - -void GTKLoop::Post(std::function fn) { - assert_true(thread_id_ != std::thread::id()); - // converting std::function to a generic pointer for gdk - gdk_threads_add_idle(_posted_fn_thunk, reinterpret_cast( - new std::function(fn))); -} - -void GTKLoop::PostDelayed(std::function fn, uint64_t delay_millis) { - gdk_threads_add_timeout( - delay_millis, _posted_fn_thunk, - reinterpret_cast(new std::function(fn))); -} - -void GTKLoop::Quit() { - assert_true(thread_id_ != std::thread::id()); - Post([]() { gtk_main_quit(); }); -} - -void GTKLoop::AwaitQuit() { quit_fence_.Wait(); } - -} // namespace ui -} // namespace xe diff --git a/src/xenia/ui/loop_win.cc b/src/xenia/ui/loop_win.cc deleted file mode 100644 index 2cd57f5fc..000000000 --- a/src/xenia/ui/loop_win.cc +++ /dev/null @@ -1,131 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2014 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#include "xenia/ui/loop_win.h" - -#include "xenia/base/assert.h" - -namespace xe { -namespace ui { - -std::unique_ptr Loop::Create() { return std::make_unique(); } - -Win32Loop::Win32Loop() : thread_id_(0) { - timer_queue_ = CreateTimerQueue(); - - xe::threading::Fence init_fence; - thread_ = std::thread([&init_fence, this]() { - xe::threading::set_name("Win32 Loop"); - thread_id_ = GetCurrentThreadId(); - - // Make a Win32 call to enable the thread queue. - MSG msg; - PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE); - - init_fence.Signal(); - - ThreadMain(); - - quit_fence_.Signal(); - }); - init_fence.Wait(); -} - -Win32Loop::~Win32Loop() { - Quit(); - thread_.join(); - - DeleteTimerQueueEx(timer_queue_, INVALID_HANDLE_VALUE); - std::lock_guard lock(pending_timers_mutex_); - while (!pending_timers_.empty()) { - auto timer = pending_timers_.back(); - pending_timers_.pop_back(); - delete timer; - } -} - -void Win32Loop::ThreadMain() { - MSG msg; - while (!should_exit_ && GetMessage(&msg, NULL, 0, 0)) { - TranslateMessage(&msg); - DispatchMessage(&msg); - - // Process queued functions. - std::lock_guard lock(posted_functions_mutex_); - for (auto it = posted_functions_.begin(); it != posted_functions_.end();) { - (*it).Call(); - it = posted_functions_.erase(it); - } - } - - UIEvent e(nullptr); - on_quit(&e); -} - -bool Win32Loop::is_on_loop_thread() { - return thread_id_ == GetCurrentThreadId(); -} - -void Win32Loop::Post(std::function fn) { - assert_true(thread_id_ != 0); - { - std::lock_guard lock(posted_functions_mutex_); - PostedFn posted_fn(fn); - posted_functions_.push_back(posted_fn); - } - - while (!PostThreadMessage(thread_id_, WM_NULL, 0, 0)) { - Sleep(1); - } -} - -void Win32Loop::TimerQueueCallback(void* context, uint8_t) { - auto timer = reinterpret_cast(context); - auto loop = timer->loop; - auto fn = std::move(timer->fn); - DeleteTimerQueueTimer(timer->timer_queue, timer->timer_handle, NULL); - { - std::lock_guard lock(loop->pending_timers_mutex_); - loop->pending_timers_.remove(timer); - } - delete timer; - loop->Post(std::move(fn)); -} - -void Win32Loop::PostDelayed(std::function fn, uint64_t delay_millis) { - if (!delay_millis) { - Post(std::move(fn)); - return; - } - auto timer = new PendingTimer(); - timer->loop = this; - timer->timer_queue = timer_queue_; - timer->fn = std::move(fn); - { - std::lock_guard lock(pending_timers_mutex_); - pending_timers_.push_back(timer); - } - CreateTimerQueueTimer(&timer->timer_handle, timer_queue_, - WAITORTIMERCALLBACK(TimerQueueCallback), timer, - DWORD(delay_millis), 0, - WT_EXECUTEINTIMERTHREAD | WT_EXECUTEONLYONCE); -} - -void Win32Loop::Quit() { - assert_true(thread_id_ != 0); - should_exit_ = true; - while (!PostThreadMessage(thread_id_, WM_NULL, 0, 0)) { - Sleep(1); - } -} - -void Win32Loop::AwaitQuit() { quit_fence_.Wait(); } - -} // namespace ui -} // namespace xe diff --git a/src/xenia/ui/loop_win.h b/src/xenia/ui/loop_win.h deleted file mode 100644 index bd2d28ae2..000000000 --- a/src/xenia/ui/loop_win.h +++ /dev/null @@ -1,74 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2014 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#ifndef XENIA_UI_LOOP_WIN_H_ -#define XENIA_UI_LOOP_WIN_H_ - -#include -#include -#include - -#include "xenia/base/platform_win.h" -#include "xenia/base/threading.h" -#include "xenia/ui/loop.h" - -namespace xe { -namespace ui { - -class Win32Loop : public Loop { - public: - Win32Loop(); - ~Win32Loop() override; - - bool is_on_loop_thread() override; - - void Post(std::function fn) override; - void PostDelayed(std::function fn, uint64_t delay_millis) override; - - void Quit() override; - void AwaitQuit() override; - - private: - struct PendingTimer { - Win32Loop* loop; - HANDLE timer_queue; - HANDLE timer_handle; - std::function fn; - }; - - class PostedFn { - public: - explicit PostedFn(std::function fn) : fn_(std::move(fn)) {} - void Call() { fn_(); } - - private: - std::function fn_; - }; - - void ThreadMain(); - - static void TimerQueueCallback(void* context, uint8_t); - - std::thread thread_; - DWORD thread_id_; - bool should_exit_ = false; - xe::threading::Fence quit_fence_; - - HANDLE timer_queue_; - std::mutex pending_timers_mutex_; - std::list pending_timers_; - - std::recursive_mutex posted_functions_mutex_; - std::list posted_functions_; -}; - -} // namespace ui -} // namespace xe - -#endif // XENIA_UI_LOOP_WIN_H_ diff --git a/src/xenia/ui/premake5.lua b/src/xenia/ui/premake5.lua index c5dbd84af..93c012600 100644 --- a/src/xenia/ui/premake5.lua +++ b/src/xenia/ui/premake5.lua @@ -13,3 +13,4 @@ project("xenia-ui") }) local_platform_files() removefiles({"*_demo.cc"}) + removefiles({"windowed_app_main_*.cc"}) diff --git a/src/xenia/ui/vulkan/premake5.lua b/src/xenia/ui/vulkan/premake5.lua index 3f675ad0a..56365f278 100644 --- a/src/xenia/ui/vulkan/premake5.lua +++ b/src/xenia/ui/vulkan/premake5.lua @@ -45,7 +45,7 @@ project("xenia-ui-window-vulkan-demo") files({ "../window_demo.cc", "vulkan_window_demo.cc", - project_root.."/src/xenia/base/main_"..platform_suffix..".cc", + project_root.."/src/xenia/ui/windowed_app_main_"..platform_suffix..".cc", }) resincludedirs({ project_root, diff --git a/src/xenia/ui/vulkan/vulkan_provider.cc b/src/xenia/ui/vulkan/vulkan_provider.cc index f9e9eea13..d67a5e263 100644 --- a/src/xenia/ui/vulkan/vulkan_provider.cc +++ b/src/xenia/ui/vulkan/vulkan_provider.cc @@ -24,8 +24,8 @@ namespace xe { namespace ui { namespace vulkan { -std::unique_ptr VulkanProvider::Create(Window* main_window) { - std::unique_ptr provider(new VulkanProvider(main_window)); +std::unique_ptr VulkanProvider::Create() { + std::unique_ptr provider(new VulkanProvider); if (!provider->Initialize()) { xe::FatalError( "Unable to initialize Vulkan graphics subsystem.\n" @@ -40,9 +40,6 @@ std::unique_ptr VulkanProvider::Create(Window* main_window) { return provider; } -VulkanProvider::VulkanProvider(Window* main_window) - : GraphicsProvider(main_window) {} - VulkanProvider::~VulkanProvider() { device_.reset(); instance_.reset(); diff --git a/src/xenia/ui/vulkan/vulkan_provider.h b/src/xenia/ui/vulkan/vulkan_provider.h index f4a8080e3..12c303785 100644 --- a/src/xenia/ui/vulkan/vulkan_provider.h +++ b/src/xenia/ui/vulkan/vulkan_provider.h @@ -25,7 +25,7 @@ class VulkanProvider : public GraphicsProvider { public: ~VulkanProvider() override; - static std::unique_ptr Create(Window* main_window); + static std::unique_ptr Create(); VulkanInstance* instance() const { return instance_.get(); } VulkanDevice* device() const { return device_.get(); } @@ -35,7 +35,7 @@ class VulkanProvider : public GraphicsProvider { std::unique_ptr CreateOffscreenContext() override; protected: - explicit VulkanProvider(Window* main_window); + VulkanProvider() = default; bool Initialize(); diff --git a/src/xenia/ui/vulkan/vulkan_window_demo.cc b/src/xenia/ui/vulkan/vulkan_window_demo.cc index 12965197b..f2e0fcff8 100644 --- a/src/xenia/ui/vulkan/vulkan_window_demo.cc +++ b/src/xenia/ui/vulkan/vulkan_window_demo.cc @@ -2,28 +2,44 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2020 Ben Vanik. All rights reserved. * + * Copyright 2021 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ +#include #include -#include -#include "xenia/base/main.h" #include "xenia/ui/vulkan/vulkan_provider.h" -#include "xenia/ui/window.h" +#include "xenia/ui/window_demo.h" +#include "xenia/ui/windowed_app.h" namespace xe { namespace ui { +namespace vulkan { -int window_demo_main(const std::vector& args); +class VulkanWindowDemoApp final : public WindowDemoApp { + public: + static std::unique_ptr Create(WindowedAppContext& app_context) { + return std::unique_ptr(new VulkanWindowDemoApp(app_context)); + } -std::unique_ptr CreateDemoGraphicsProvider(Window* window) { - return xe::ui::vulkan::VulkanProvider::Create(window); + protected: + std::unique_ptr CreateGraphicsProvider() const override; + + private: + explicit VulkanWindowDemoApp(WindowedAppContext& app_context) + : WindowDemoApp(app_context, "xenia-ui-window-vulkan-demo") {} +}; + +std::unique_ptr VulkanWindowDemoApp::CreateGraphicsProvider() + const { + return VulkanProvider::Create(); } +} // namespace vulkan } // namespace ui } // namespace xe -DEFINE_ENTRY_POINT("xenia-ui-window-vulkan-demo", xe::ui::window_demo_main, ""); +XE_DEFINE_WINDOWED_APP(xenia_ui_window_vulkan_demo, + xe::ui::vulkan::VulkanWindowDemoApp::Create); diff --git a/src/xenia/ui/window.cc b/src/xenia/ui/window.cc index 47a2b874c..74b81ae21 100644 --- a/src/xenia/ui/window.cc +++ b/src/xenia/ui/window.cc @@ -31,8 +31,8 @@ constexpr double kDoubleClickDistance = 5; constexpr int32_t kMouseWheelDetent = 120; -Window::Window(Loop* loop, const std::string& title) - : loop_(loop), title_(title) {} +Window::Window(WindowedAppContext& app_context, const std::string& title) + : app_context_(app_context), title_(title) {} Window::~Window() { // Context must have been cleaned up already in OnDestroy. diff --git a/src/xenia/ui/window.h b/src/xenia/ui/window.h index 56094553b..2627c7ca0 100644 --- a/src/xenia/ui/window.h +++ b/src/xenia/ui/window.h @@ -16,10 +16,10 @@ #include "xenia/base/delegate.h" #include "xenia/base/platform.h" #include "xenia/ui/graphics_context.h" -#include "xenia/ui/loop.h" #include "xenia/ui/menu_item.h" #include "xenia/ui/ui_event.h" #include "xenia/ui/window_listener.h" +#include "xenia/ui/windowed_app_context.h" namespace xe { namespace ui { @@ -31,11 +31,13 @@ class ImGuiDrawer; class Window { public: - static std::unique_ptr Create(Loop* loop, const std::string& title); + static std::unique_ptr Create(WindowedAppContext& app_context_, + const std::string& title); virtual ~Window(); - Loop* loop() const { return loop_; } + WindowedAppContext& app_context() const { return app_context_; } + virtual NativePlatformHandle native_platform_handle() const = 0; virtual NativeWindowHandle native_handle() const = 0; @@ -69,8 +71,11 @@ class Window { virtual bool is_bordered() const { return false; } virtual void set_bordered(bool enabled) {} - virtual int get_dpi() const { return 96; } - virtual float get_dpi_scale() const { return get_dpi() / 96.f; } + virtual int get_medium_dpi() const { return 96; } + virtual int get_dpi() const { return get_medium_dpi(); } + virtual float get_dpi_scale() const { + return float(get_dpi()) / float(get_medium_dpi()); + } bool has_focus() const { return has_focus_; } virtual void set_focus(bool value) { has_focus_ = value; } @@ -78,6 +83,8 @@ class Window { bool is_cursor_visible() const { return is_cursor_visible_; } virtual void set_cursor_visible(bool value) { is_cursor_visible_ = value; } + // TODO(Triang3l): Don't scale for guest output - use physical pixels. Use + // logical pixels only for the immediate drawer. int32_t width() const { return width_; } int32_t height() const { return height_; } int32_t scaled_width() const { return int32_t(width_ * get_dpi_scale()); } @@ -135,7 +142,7 @@ class Window { Delegate on_mouse_wheel; protected: - Window(Loop* loop, const std::string& title); + Window(WindowedAppContext& app_context, const std::string& title); void ForEachListener(std::function fn); void TryForEachListener(std::function fn); @@ -170,7 +177,7 @@ class Window { void OnKeyPress(KeyEvent* e, bool is_down, bool is_char); - Loop* loop_ = nullptr; + WindowedAppContext& app_context_; std::unique_ptr main_menu_; std::string title_; #ifdef XE_PLATFORM_GNU_LINUX diff --git a/src/xenia/ui/window_demo.cc b/src/xenia/ui/window_demo.cc index 4f94a1fc3..05d61611d 100644 --- a/src/xenia/ui/window_demo.cc +++ b/src/xenia/ui/window_demo.cc @@ -2,7 +2,7 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2020 Ben Vanik. All rights reserved. * + * Copyright 2021 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ @@ -12,7 +12,6 @@ #include "third_party/imgui/imgui.h" #include "xenia/base/clock.h" #include "xenia/base/logging.h" -#include "xenia/base/main.h" #include "xenia/base/profiling.h" #include "xenia/base/threading.h" #include "xenia/ui/graphics_provider.h" @@ -20,28 +19,29 @@ #include "xenia/ui/imgui_drawer.h" #include "xenia/ui/virtual_key.h" #include "xenia/ui/window.h" +#include "xenia/ui/window_demo.h" namespace xe { namespace ui { -// Implemented in one of the window_*_demo.cc files under a subdir. -std::unique_ptr CreateDemoGraphicsProvider(Window* window); +WindowDemoApp::~WindowDemoApp() { Profiler::Shutdown(); } -int window_demo_main(const std::vector& args) { +bool WindowDemoApp::OnInitialize() { Profiler::Initialize(); - Profiler::ThreadEnter("main"); + Profiler::ThreadEnter("Main"); - // Create run loop and the window. - auto loop = ui::Loop::Create(); - auto window = xe::ui::Window::Create(loop.get(), GetEntryInfo().name); - loop->PostSynchronous([&window]() { - xe::threading::set_name("Win32 Loop"); - xe::Profiler::ThreadEnter("Win32 Loop"); - if (!window->Initialize()) { - FatalError("Failed to initialize main window"); - return; - } - }); + // Create graphics provider that provides the context for the window. + graphics_provider_ = CreateGraphicsProvider(); + if (!graphics_provider_) { + return false; + } + + // Create the window. + window_ = xe::ui::Window::Create(app_context(), GetName()); + if (!window_->Initialize()) { + XELOGE("Failed to initialize main window"); + return false; + } // Main menu. auto main_menu = MenuItem::Create(MenuItem::Type::kNormal); @@ -49,7 +49,7 @@ int window_demo_main(const std::vector& args) { { file_menu->AddChild(MenuItem::Create(MenuItem::Type::kString, "&Close", "Alt+F4", - [&window]() { window->Close(); })); + [this]() { window_->Close(); })); } main_menu->AddChild(std::move(file_menu)); auto debug_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Debug"); @@ -62,37 +62,28 @@ int window_demo_main(const std::vector& args) { []() { Profiler::TogglePause(); })); } main_menu->AddChild(std::move(debug_menu)); - window->set_main_menu(std::move(main_menu)); + window_->set_main_menu(std::move(main_menu)); // Initial size setting, done here so that it knows the menu exists. - window->Resize(1920, 1200); + window_->Resize(1920, 1200); - // Create the graphics context used for drawing and setup the window. - std::unique_ptr graphics_provider; - loop->PostSynchronous([&window, &graphics_provider]() { - // Create graphics provider and an initial context for the window. - // The window will finish initialization with the context (loading - // resources, etc). - graphics_provider = CreateDemoGraphicsProvider(window.get()); - window->set_context(graphics_provider->CreateContext(window.get())); + // Create the graphics context for the window. The window will finish + // initialization with the context (loading resources, etc). + window_->set_context(graphics_provider_->CreateContext(window_.get())); - // Setup the profiler display. - GraphicsContextLock context_lock(window->context()); - Profiler::set_window(window.get()); + // Setup the profiler display. + GraphicsContextLock context_lock(window_->context()); + Profiler::set_window(window_.get()); - // Enable imgui input. - window->set_imgui_input_enabled(true); + // Enable imgui input. + window_->set_imgui_input_enabled(true); + + window_->on_closed.AddListener([this](xe::ui::UIEvent* e) { + XELOGI("User-initiated death!"); + app_context().QuitFromUIThread(); }); - window->on_closed.AddListener( - [&loop, &window, &graphics_provider](xe::ui::UIEvent* e) { - loop->Quit(); - Profiler::Shutdown(); - XELOGI("User-initiated death!"); - }); - loop->on_quit.AddListener([&window](xe::ui::UIEvent* e) { window.reset(); }); - - window->on_key_down.AddListener([](xe::ui::KeyEvent* e) { + window_->on_key_down.AddListener([](xe::ui::KeyEvent* e) { switch (e->virtual_key()) { case VirtualKey::kF3: Profiler::ToggleDisplay(); @@ -102,23 +93,19 @@ int window_demo_main(const std::vector& args) { } }); - window->on_painting.AddListener([&](xe::ui::UIEvent* e) { - auto& io = window->imgui_drawer()->GetIO(); + window_->on_painting.AddListener([this](xe::ui::UIEvent* e) { + auto& io = window_->imgui_drawer()->GetIO(); ImGui::ShowDemoWindow(); ImGui::ShowMetricsWindow(); + Profiler::Flip(); + // Continuous paint. - window->Invalidate(); + window_->Invalidate(); }); - // Wait until we are exited. - loop->AwaitQuit(); - - window.reset(); - loop.reset(); - graphics_provider.reset(); - return 0; + return true; } } // namespace ui diff --git a/src/xenia/ui/window_demo.h b/src/xenia/ui/window_demo.h new file mode 100644 index 000000000..29b6bc1fa --- /dev/null +++ b/src/xenia/ui/window_demo.h @@ -0,0 +1,44 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_WINDOW_DEMO_H_ +#define XENIA_UI_WINDOW_DEMO_H_ + +#include +#include + +#include "xenia/ui/graphics_provider.h" +#include "xenia/ui/window.h" +#include "xenia/ui/windowed_app.h" + +namespace xe { +namespace ui { + +class WindowDemoApp : public WindowedApp { + public: + ~WindowDemoApp(); + + bool OnInitialize() override; + + protected: + explicit WindowDemoApp(WindowedAppContext& app_context, + const std::string_view name) + : WindowedApp(app_context, name) {} + + virtual std::unique_ptr CreateGraphicsProvider() const = 0; + + private: + std::unique_ptr graphics_provider_; + std::unique_ptr window_; +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_WINDOW_DEMO_H_ diff --git a/src/xenia/ui/window_gtk.cc b/src/xenia/ui/window_gtk.cc index cf272619b..b35f91305 100644 --- a/src/xenia/ui/window_gtk.cc +++ b/src/xenia/ui/window_gtk.cc @@ -22,12 +22,13 @@ namespace xe { namespace ui { -std::unique_ptr Window::Create(Loop* loop, const std::string& title) { - return std::make_unique(loop, title); +std::unique_ptr Window::Create(WindowedAppContext& app_context, + const std::string& title) { + return std::make_unique(app_context, title); } -GTKWindow::GTKWindow(Loop* loop, const std::string& title) - : Window(loop, title) {} +GTKWindow::GTKWindow(WindowedAppContext& app_context, const std::string& title) + : Window(app_context, title) {} GTKWindow::~GTKWindow() { OnDestroy(); @@ -92,7 +93,7 @@ gboolean close_callback(GtkWidget* widget, gpointer data) { return G_SOURCE_CONTINUE; } -void GTKWindow::Create() { +bool GTKWindow::OnCreate() { // GTK optionally allows passing argv and argc here for parsing gtk specific // options. We won't bother window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); @@ -129,10 +130,7 @@ void GTKWindow::Create() { gtk_box_pack_end(GTK_BOX(box_), drawing_area_, TRUE, TRUE, 0); gtk_container_add(GTK_CONTAINER(window_), box_); gtk_widget_show_all(window_); -} -bool GTKWindow::OnCreate() { - loop()->PostSynchronous([this]() { this->Create(); }); return super::OnCreate(); } diff --git a/src/xenia/ui/window_gtk.h b/src/xenia/ui/window_gtk.h index 27a4c08b0..6677f2fc7 100644 --- a/src/xenia/ui/window_gtk.h +++ b/src/xenia/ui/window_gtk.h @@ -28,7 +28,7 @@ class GTKWindow : public Window { using super = Window; public: - GTKWindow(Loop* loop, const std::string& title); + GTKWindow(WindowedAppContext& app_context, const std::string& title); ~GTKWindow() override; NativePlatformHandle native_platform_handle() const override { @@ -74,7 +74,6 @@ class GTKWindow : public Window { void OnResize(UIEvent* e) override; private: - void Create(); GtkWidget* window_; GtkWidget* box_; GtkWidget* drawing_area_; diff --git a/src/xenia/ui/window_win.cc b/src/xenia/ui/window_win.cc index a88cad529..a6d2c68ae 100644 --- a/src/xenia/ui/window_win.cc +++ b/src/xenia/ui/window_win.cc @@ -18,16 +18,19 @@ #include "xenia/base/logging.h" #include "xenia/base/platform_win.h" #include "xenia/ui/virtual_key.h" +#include "xenia/ui/windowed_app_context_win.h" namespace xe { namespace ui { -std::unique_ptr Window::Create(Loop* loop, const std::string& title) { - return std::make_unique(loop, title); +std::unique_ptr Window::Create(WindowedAppContext& app_context, + const std::string& title) { + return std::make_unique(app_context, title); } -Win32Window::Win32Window(Loop* loop, const std::string& title) - : Window(loop, title) {} +Win32Window::Win32Window(WindowedAppContext& app_context, + const std::string& title) + : Window(app_context, title) {} Win32Window::~Win32Window() { OnDestroy(); @@ -43,37 +46,32 @@ Win32Window::~Win32Window() { } NativePlatformHandle Win32Window::native_platform_handle() const { - return ::GetModuleHandle(nullptr); + return static_cast(app_context()).hinstance(); } bool Win32Window::Initialize() { return OnCreate(); } bool Win32Window::OnCreate() { - HINSTANCE hInstance = GetModuleHandle(nullptr); + HINSTANCE hInstance = + static_cast(app_context()).hinstance(); - if (!SetProcessDpiAwareness_ || !GetDpiForMonitor_) { + // Per-monitor DPI awareness is expected to be enabled via the manifest, as + // that's the recommended way, which also doesn't require calling + // SetProcessDpiAwareness before doing anything that may depend on DPI + // awareness (so it's safe to use any Windows APIs before this code). + // TODO(Triang3l): Safe handling of per-monitor DPI awareness v2, with + // automatic scaling on DPI change. + if (!GetDpiForMonitor_) { auto shcore = GetModuleHandleW(L"shcore.dll"); if (shcore) { - SetProcessDpiAwareness_ = - GetProcAddress(shcore, "SetProcessDpiAwareness"); GetDpiForMonitor_ = GetProcAddress(shcore, "GetDpiForMonitor"); } } static bool has_registered_class = false; if (!has_registered_class) { - // Tell Windows that we're DPI aware. - if (SetProcessDpiAwareness_) { - auto spda = (decltype(&SetProcessDpiAwareness))SetProcessDpiAwareness_; - auto res = spda(PROCESS_PER_MONITOR_DPI_AWARE); - if (res != S_OK) { - XELOGW("Failed to set process DPI awareness. (code = 0x{:08X})", - static_cast(res)); - } - } - - WNDCLASSEX wcex; - wcex.cbSize = sizeof(WNDCLASSEX); + WNDCLASSEXW wcex; + wcex.cbSize = sizeof(wcex); wcex.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; wcex.lpfnWndProc = Win32Window::WndProcThunk; wcex.cbClsExtra = 0; @@ -85,7 +83,7 @@ bool Win32Window::OnCreate() { wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wcex.lpszMenuName = nullptr; wcex.lpszClassName = L"XeniaWindowClass"; - if (!RegisterClassEx(&wcex)) { + if (!RegisterClassExW(&wcex)) { XELOGE("RegisterClassEx failed"); return false; } @@ -216,7 +214,9 @@ bool Win32Window::SetIcon(const void* buffer, size_t size) { } // Reset icon to default. - auto default_icon = LoadIconW(GetModuleHandle(nullptr), L"MAINICON"); + auto default_icon = LoadIconW( + static_cast(app_context()).hinstance(), + L"MAINICON"); SendMessageW(hwnd_, WM_SETICON, ICON_BIG, reinterpret_cast(default_icon)); SendMessageW(hwnd_, WM_SETICON, ICON_SMALL, @@ -316,13 +316,22 @@ void Win32Window::set_bordered(bool enabled) { } int Win32Window::get_dpi() const { + // TODO(Triang3l): Cache until WM_DPICHANGED is received (which, with + // per-monitor awareness v2 will also receive the new suggested window size). + // According to MSDN, x and y are identical. + if (!GetDpiForMonitor_) { - return 96; + HDC screen_hdc = GetDC(nullptr); + if (!screen_hdc) { + return get_medium_dpi(); + } + int logical_pixels_x = GetDeviceCaps(screen_hdc, LOGPIXELSX); + ReleaseDC(nullptr, screen_hdc); + return logical_pixels_x; } HMONITOR monitor = MonitorFromWindow(hwnd_, MONITOR_DEFAULTTOPRIMARY); - // According to msdn, x and y are identical... UINT dpi_x, dpi_y; auto gdfm = (decltype(&GetDpiForMonitor))GetDpiForMonitor_; gdfm(monitor, MDT_DEFAULT, &dpi_x, &dpi_y); diff --git a/src/xenia/ui/window_win.h b/src/xenia/ui/window_win.h index fe16d943f..f4dceaeac 100644 --- a/src/xenia/ui/window_win.h +++ b/src/xenia/ui/window_win.h @@ -24,7 +24,7 @@ class Win32Window : public Window { using super = Window; public: - Win32Window(Loop* loop, const std::string& title); + Win32Window(WindowedAppContext& app_context, const std::string& title); ~Win32Window() override; NativePlatformHandle native_platform_handle() const override; @@ -90,7 +90,6 @@ class Win32Window : public Window { WINDOWPLACEMENT windowed_pos_ = {0}; POINT last_mouse_pos_ = {0}; - void* SetProcessDpiAwareness_ = nullptr; void* GetDpiForMonitor_ = nullptr; }; diff --git a/src/xenia/ui/windowed_app.h b/src/xenia/ui/windowed_app.h new file mode 100644 index 000000000..14eba9cb7 --- /dev/null +++ b/src/xenia/ui/windowed_app.h @@ -0,0 +1,129 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_WINDOWED_APP_H_ +#define XENIA_UI_WINDOWED_APP_H_ + +#include +#include +#include +#include + +#include "xenia/base/platform.h" +#include "xenia/ui/windowed_app_context.h" + +#if XE_PLATFORM_ANDROID +#include + +#include "xenia/ui/windowed_app_context_android.h" +#endif + +namespace xe { +namespace ui { + +// Interface between the platform's entry points (in the main, UI, thread that +// also runs the message loop) and the app that implements it. +class WindowedApp { + public: + // WindowedApps are expected to provide a static creation function, for + // creating an instance of the class (which may be called before + // initialization of platform-specific parts, should preferably be as simple + // as possible). + + WindowedApp(const WindowedApp& app) = delete; + WindowedApp& operator=(const WindowedApp& app) = delete; + virtual ~WindowedApp() = default; + + WindowedAppContext& app_context() const { return app_context_; } + + // Same as the executable (project), xenia-library-app. + const std::string& GetName() const { return name_; } + const std::string& GetPositionalOptionsUsage() const { + return positional_options_usage_; + } + const std::vector& GetPositionalOptions() const { + return positional_options_; + } + + // Called once before receiving other lifecycle callback invocations. Cvars + // will be initialized with the launch arguments. Returns whether the app has + // been initialized successfully (otherwise platform-specific code must call + // OnDestroy and refuse to continue running the app). + virtual bool OnInitialize() = 0; + // See OnDestroy for more info. + void InvokeOnDestroy() { + // For safety and convenience of referencing objects owned by the app in + // pending functions queued in or after OnInitialize, make sure they are + // executed before telling the app that destruction needs to happen. + app_context().ExecutePendingFunctionsFromUIThread(); + OnDestroy(); + } + + protected: + // Positional options should be initialized in the constructor if needed. + // Cvars will not have been initialized with the arguments at the moment of + // construction (as the result depends on construction). + explicit WindowedApp( + WindowedAppContext& app_context, const std::string_view name, + const std::string_view positional_options_usage = std::string_view()) + : app_context_(app_context), + name_(name), + positional_options_usage_(positional_options_usage) {} + + // For calling from the constructor. + void AddPositionalOption(const std::string_view option) { + positional_options_.emplace_back(option); + } + + // OnDestroy entry point may be called (through InvokeOnDestroy) by the + // platform-specific lifecycle interface at request of either the app itself + // or the OS - thus should be possible for the lifecycle interface to call at + // any moment (not from inside other lifecycle callbacks though). The app will + // also be destroyed when that happens, so the destructor will also be called + // (but this is more safe with respect to exceptions). This is only guaranteed + // to be called if OnInitialize has already happened (successfully or not) - + // in case of an error before initialization, the destructor may be called + // alone as well. Context's pending functions will be executed before the + // call, so it's safe to destroy dependencies of them here (though it may + // still be possible to add more pending functions here depending on whether + // the context was explicitly shut down before this is invoked). + virtual void OnDestroy() {} + + private: + WindowedAppContext& app_context_; + + std::string name_; + std::string positional_options_usage_; + std::vector positional_options_; +}; + +#if XE_PLATFORM_ANDROID +// Multiple apps in a single library. ANativeActivity_onCreate chosen via +// android.app.func_name of the NativeActivity of each app. +#define XE_DEFINE_WINDOWED_APP(export_name, creator) \ + __attribute__((visibility("default"))) extern "C" void export_name( \ + ANativeActivity* activity, void* saved_state, size_t saved_state_size) { \ + xe::ui::AndroidWindowedAppContext::StartAppOnNativeActivityCreate( \ + activity, saved_state, saved_state_size, creator); \ + } +#else +// Separate executables for each app. +std::unique_ptr (*GetWindowedAppCreator())( + WindowedAppContext& app_context); +#define XE_DEFINE_WINDOWED_APP(export_name, creator) \ + std::unique_ptr (*xe::ui::GetWindowedAppCreator())( \ + xe::ui::WindowedAppContext & app_context) { \ + return creator; \ + } +#endif + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_WINDOWED_APP_H_ diff --git a/src/xenia/ui/windowed_app_context.cc b/src/xenia/ui/windowed_app_context.cc new file mode 100644 index 000000000..ee7c3d619 --- /dev/null +++ b/src/xenia/ui/windowed_app_context.cc @@ -0,0 +1,180 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/windowed_app_context.h" + +#include + +#include "xenia/base/assert.h" +#include "xenia/base/threading.h" + +namespace xe { +namespace ui { + +WindowedAppContext::~WindowedAppContext() { + // The UI thread is responsible for managing the lifetime of the context. + assert_true(IsInUIThread()); + + // It's okay to destroy the context from a platform's internal UI loop + // callback, primarily on platforms where the loop is run by the OS itself, + // and the context can't be created and destroyed in a RAII way, rather, it's + // created in an initialization handler and destroyed in a shutdown handler + // called by the OS. However, destruction must not be done from within the + // queued functions - as in this case, the pending function container, the + // mutex, will be accessed after having been destroyed already. + + // Make sure CallInUIThreadDeferred doesn't call + // NotifyUILoopOfPendingFunctions, which is virtual. + is_in_destructor_ = true; + // Make sure the final ExecutePendingFunctionsFromUIThread doesn't call + // PlatformQuitFromUIThread, which is virtual. + has_quit_ = true; + + // Platform-specific quit is expected to be performed by the subclass (the + // order of it vs. the final ExecutePendingFunctionsFromUIThread shouldn't + // matter anymore, the implementation may assume that no pending functions + // will be requested for execution specifically via the platform-specific + // loop, as there should be no more references to the context in other + // threads), can't call the virtual PlatformQuitFromUIThread anymore. + ExecutePendingFunctionsFromUIThread(true); +} + +bool WindowedAppContext::CallInUIThreadDeferred( + std::function function) { + { + std::unique_lock pending_functions_lock( + pending_functions_mutex_); + if (!pending_functions_accepted_) { + // Will not be called as the loop will not be executed anymore. + return false; + } + pending_functions_.emplace_back(std::move(function)); + } + // Notify unconditionally, even if currently running pending functions. It's + // possible for pending functions themselves to run inner platform message + // loops, such as when displaying dialogs - in this case, the notification is + // needed to run the new function from such an inner loop. A modal loop can be + // started even in leftovers happening during the quit, where there's still + // opportunity for enqueueing and executing new pending functions - so only + // checking if called in the destructor (it's safe to check this without + // locking a mutex as it's assumed that if the object is already being + // destroyed, no other threads can have references to it - any access would + // result in a race condition anyway) as the subclass has already been + // destroyed. Having pending_functions_mutex_ unlocked also means that + // NotifyUILoopOfPendingFunctions may be done while the UI thread is calling + // or has already called PlatformQuitFromUIThread - but it's better than + // keeping pending_functions_mutex_ locked as NotifyUILoopOfPendingFunctions + // may be implemented as pushing to a fixed-size pipe, in which case it will + // have to wait until free space is available, but if the UI thread tries to + // lock the mutex afterwards to execute pending functions (and encouters + // contention), nothing will be able to receive from the pipe anymore and thus + // free the space, causing a deadlock. + if (!is_in_destructor_) { + NotifyUILoopOfPendingFunctions(); + } + return true; +} + +bool WindowedAppContext::CallInUIThread(std::function function) { + if (IsInUIThread()) { + // The intention is just to make sure the code is executed in the UI thread, + // don't defer execution if no need to. + function(); + return true; + } + return CallInUIThreadDeferred(std::move(function)); +} + +bool WindowedAppContext::CallInUIThreadSynchronous( + std::function function) { + if (IsInUIThread()) { + // Prevent deadlock if called from the UI thread. + function(); + return true; + } + xe::threading::Fence fence; + if (!CallInUIThreadDeferred([&function, &fence]() { + function(); + fence.Signal(); + })) { + return false; + } + fence.Wait(); + return true; +} + +void WindowedAppContext::QuitFromUIThread() { + assert_true(IsInUIThread()); + bool has_quit_previously = has_quit_; + // Make sure PlatformQuitFromUIThread is called only once, not from nested + // pending function execution during the quit - otherwise it will be called + // when it's still possible to add new pending functions. This isn't as wrong + // as calling PlatformQuitFromUIThread from the destructor, but still a part + // of the contract for simplicity. + has_quit_ = true; + // Executing pending function unconditionally because it's the contract of + // this method that functions are executed immediately. + ExecutePendingFunctionsFromUIThread(true); + if (has_quit_previously) { + // Potentially calling QuitFromUIThread from inside a pending function (in + // the worst and dangerous case, from a pending function executed in the + // destructor - and PlatformQuitFromUIThread is virtual). + return; + } + // Call the platform-specific shutdown while letting it assume that no new + // functions will be queued anymore (but NotifyUILoopOfPendingFunctions may + // still be called after PlatformQuitFromUIThread as the two are not + // interlocked). This is different than the order in the destruction, but + // there this assumption is ensured by the expectation that there should be no + // more references to the context in other threads that would allow queueing + // new functions with calling NotifyUILoopOfPendingFunctions. + PlatformQuitFromUIThread(); +} + +void WindowedAppContext::ExecutePendingFunctionsFromUIThread(bool is_final) { + assert_true(IsInUIThread()); + std::unique_lock pending_functions_lock(pending_functions_mutex_); + while (!pending_functions_.empty()) { + // Removing the function from the queue before executing it, as the function + // itself may call ExecutePendingFunctionsFromUIThread - if it's kept, the + // inner loop will try to execute it again, resulting in potentially endless + // recursion, and even if it's terminated, each level will be trying to + // remove the same function from the queue - instead, actually removing + // other functions, or even beyond the end of the queue. + std::function function = std::move(pending_functions_.front()); + pending_functions_.pop_front(); + // Call the function with the lock released as it may take an indefinitely + // long time to execute if it opens some dialog (possibly with its own + // platform message loop), and in that case, without unlocking, no other + // thread would be able to add new pending functions (which would result in + // unintended waits for user input). This also allows using std::mutex + // instead of std::recursive_mutex. + pending_functions_lock.unlock(); + function(); + pending_functions_lock.lock(); + } + if (is_final) { + // Atomically with completion of the pending functions loop, disallow adding + // new functions after executing the existing ones - it was possible to + // enqueue new functions from the leftover ones as there still was + // opportunity to call them, so it wasn't necessary to disallow adding + // before executing, but now new functions will potentially never be + // executed. This is done even if this is just an inner pending functions + // execution and there's still potential possibility of adding and executing + // new functions in the outer loops - for simplicity and consistency (so + // QuitFromUIThread's behavior doesn't depend as much on the location of the + // call - inside a pending function or from some system callback of the + // window), assuming after a PlatformQuitFromUIThread call, it's not + // possible to add new pending functions anymore. + pending_functions_accepted_ = false; + } +} + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/windowed_app_context.h b/src/xenia/ui/windowed_app_context.h new file mode 100644 index 000000000..eb377cc20 --- /dev/null +++ b/src/xenia/ui/windowed_app_context.h @@ -0,0 +1,184 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_WINDOWED_APP_CONTEXT_H_ +#define XENIA_UI_WINDOWED_APP_CONTEXT_H_ + +#include +#include +#include +#include +#include + +#include "xenia/base/assert.h" + +namespace xe { +namespace ui { + +// Context for inputs provided by the entry point and interacting with the +// platform's UI loop, to be implemented by platforms. +class WindowedAppContext { + public: + WindowedAppContext(const WindowedAppContext& context) = delete; + WindowedAppContext& operator=(const WindowedAppContext& context) = delete; + virtual ~WindowedAppContext(); + + // The thread where the object is created will be assumed to be the UI thread, + // for the purpose of being able to perform CallInUIThreadSynchronous before + // running the loop. + bool IsInUIThread() const { + return std::this_thread::get_id() == ui_thread_id_; + } + + // CallInUIThreadDeferred and CallInUIThread are fire and forget - will be + // executed at some point the future when the UI thread is running the loop + // and is not busy doing other things. + // Therefore, references to objects in the function may outlive the owners of + // those objects, so use-after-free is very easy to create if not being + // careful enough. + // There are two solutions to this issue: + // - Signaling a fence in the function, awaiting it before destroying objects + // referenced by the function (works for shutdown from non-UI threads, or + // for CallInUIThread, but not CallInUIThreadDeferred, in the UI thread). + // - Calling ExecutePendingFunctionsFromUIThread in the UI thread before + // destroying objects referenced by the function (works for shutdown from + // the UI thread, though CallInUIThreadSynchronous doing + // ExecutePendingFunctionsFromUIThread is also an option for shutdown from + // any thread). + // (These are not required if all the called function is doing is triggering a + // quit with the context pointer captured by value, as the only object + // involved will be the context itself, with a pointer that is valid until + // it's destroyed - the most late location of the pending function execution + // possible.) + // Returning true if the function has been enqueued (it will be called at some + // point, at worst, before exiting the loop) or called immediately, false if + // it was dropped (if calling after exiting the loop). + // It's okay to enqueue functions from queued functions already being + // executed. As execution may be happening during the destruction of the + // context as well, in this case, CallInUIThreadDeferred must make sure it + // doesn't call anything virtual in the destructor. + + // Enqueues the function regardless of the current thread. Won't necessarily + // be executed directly from the platform main loop as + // ExecutePendingFunctionsFromUIThread may be called anywhere (for instance, + // to await pending functions before destroying what they are referencing). + bool CallInUIThreadDeferred(std::function function); + // Executes the function immediately if already in the UI thread, enqueues it + // otherwise. + bool CallInUIThread(std::function function); + bool CallInUIThreadSynchronous(std::function function); + // It's okay to call this function from the queued functions themselves (such + // as in the case of waiting for a pending async CallInUIThreadDeferred + // described above - the wait may be done inside a CallInUIThreadSynchronous + // function safely). + void ExecutePendingFunctionsFromUIThread() { + ExecutePendingFunctionsFromUIThread(false); + } + + // If on the target platform, the program itself is supposed to run the UI + // loop, this may be checked before doing blocking message waits as an + // additional safety measure beyond what PlatformQuitFromUIThread guarantees, + // and if true, the loop should be terminated (pending function will already + // have been executed). This doesn't imply that pending functions have been + // executed in all contexts, however - they can be executing from quitting + // itself (in the worst case, in the destructor where virtual methods can't be + // called), and in this case, this will be returning true. + bool HasQuitFromUIThread() const { + assert_true(IsInUIThread()); + return has_quit_; + } + + // Immediately disallows adding new pending UI functions, executes the already + // queued ones, and makes sure that the UI loop is aware that it was asked to + // stop running. This must not destroy the context or the app directly - the + // actual app shutdown will be initiated at some point outside the scope of + // app's callbacks. May call virtual functions - invoke + // ExecutePendingFunctionsFromUIThread(true) in the destructor instead, and + // use the platform-specific destructor to invoke the needed + // PlatformQuitFromUIThread logic (it should be safe, in case of destruction, + // to perform platform-specific quit request logic before the common part - + // NotifyUILoopOfPendingFunctions won't be called after the platform-specific + // quit request logic in this case anyway as destruction expects that there + // are no references to the object in other threads). Safe to call from within + // pending functions - requesting quit from non-UI threads is possible via + // methods like CallInUIThreadSynchronous. For deferred, rather than + // immediate, quitting from the UI thread, CallInUIThreadDeferred may be used + // (but the context pointer should be captured by value not to require + // explicit completion forcing in case the storage of the pointer is lost + // before the function is called). + void QuitFromUIThread(); + // Callable from any thread. This is a special case where a completely + // fire-and-forget CallInUIThreadDeferred is safe, as the function only + // references nonvirtual functions of the context itself, and will be called + // at most from the destructor. No need to return the result - it doesn't + // matter if has quit already or not, as that's the intention anyway. + void RequestDeferredQuit() { + CallInUIThreadDeferred([this] { QuitFromUIThread(); }); + } + + protected: + WindowedAppContext() : ui_thread_id_(std::this_thread::get_id()) {} + + // Can be called from any thread (including the UI thread) to ask the OS to + // run an iteration of the UI loop (with or without processing internal UI + // messages, this is platform-dependent) so pending functions will be executed + // at some point. pending_functions_mutex_ will not be locked when this is + // called (to make sure that, for example, the caller, waiting for space in + // the OS's message queue, such as in case of a Linux pipe, won't be blocking + // the UI thread that has started executing pending messages while pumping + // that pipe, resulting in a deadlock) - implementations don't directly see + // anything protected by it anyway, and a spurious notification shouldn't be + // causing any damage, this is similar to how condition variables can be + // signaled outside the critical section (signaling inside the critical + // section may also cause contention if the thread waiting is woken up quickly + // enough). This, however, means that NotifyUILoopOfPendingFunctions may be + // called in a non-UI thread after the final pending message processing + // followed by PlatformQuitFromUIThread in the UI thread - so it can still be + // called after a quit. + virtual void NotifyUILoopOfPendingFunctions() = 0; + + // Called when requesting a quit in the UI thread to tell the platform that + // the UI loop needs to be terminated. The pending function queue is assumed + // to be empty before this is called, and no new pending functions can be + // added (but NotifyUILoopOfPendingFunctions may still be called as it's not + // mutually exclusive with PlatformQuitFromUIThread - if this matters, the + // platform implementation itself should be resolving this case). + virtual void PlatformQuitFromUIThread() = 0; + + std::thread::id ui_thread_id_; + + private: + // May be called with is_final == true from the destructor - must not call + // anything virtual in this case. + void ExecutePendingFunctionsFromUIThread(bool is_final); + + // Accessible by the UI thread. + bool has_quit_ = false; + bool is_in_destructor_ = false; + + // Synchronizes producers with each other and with the consumer, as well as + // all of them with the pending_functions_accepted_ variable which indicates + // whether shutdown has not been performed yet, as after the shutdown, no new + // functions will be executed anymore, therefore no new function should be + // queued, as it will never be called (thus CallInUIThreadSynchronous for it + // will never return, for instance). + std::mutex pending_functions_mutex_; + std::deque> pending_functions_; + // Protected by pending_functions_mutex_, writable by the UI thread, readable + // by any thread. Must be set to false before exiting the main platform loop, + // but before that, all pending functions must be executed no matter what, as + // they may need to signal fences currently being awaited (like the + // CallInUIThreadSynchronous fence). + bool pending_functions_accepted_ = true; +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_WINDOWED_APP_CONTEXT_H_ diff --git a/src/xenia/ui/windowed_app_context_android.cc b/src/xenia/ui/windowed_app_context_android.cc new file mode 100644 index 000000000..e2dbe1aa5 --- /dev/null +++ b/src/xenia/ui/windowed_app_context_android.cc @@ -0,0 +1,67 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/windowed_app_context_android.h" + +#include + +#include "xenia/base/assert.h" +#include "xenia/ui/windowed_app.h" + +namespace xe { +namespace ui { + +void AndroidWindowedAppContext::StartAppOnNativeActivityCreate( + ANativeActivity* activity, [[maybe_unused]] void* saved_state, + [[maybe_unused]] size_t saved_state_size, + std::unique_ptr (*app_creator)( + WindowedAppContext& app_context)) { + AndroidWindowedAppContext* app_context = + new AndroidWindowedAppContext(activity); + // The pointer is now held by the Activity as its ANativeActivity::instance, + // until the destruction. + if (!app_context->InitializeApp(app_creator)) { + delete app_context; + ANativeActivity_finish(activity); + } +} + +AndroidWindowedAppContext::~AndroidWindowedAppContext() { + // TODO(Triang3l): Unregister activity callbacks. + activity_->instance = nullptr; +} + +void AndroidWindowedAppContext::NotifyUILoopOfPendingFunctions() { + // TODO(Triang3l): Request message processing in the UI thread. +} + +void AndroidWindowedAppContext::PlatformQuitFromUIThread() { + ANativeActivity_finish(activity); +} + +AndroidWindowedAppContext::AndroidWindowedAppContext(ANativeActivity* activity) + : activity_(activity) { + activity_->instance = this; + // TODO(Triang3l): Register activity callbacks. +} + +bool AndroidWindowedAppContext::InitializeApp(std::unique_ptr ( + *app_creator)(WindowedAppContext& app_context)) { + assert_null(app_); + app_ = app_creator(this); + if (!app_->OnInitialize()) { + app_->InvokeOnDestroy(); + app_.reset(); + return false; + } + return true; +} + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/windowed_app_context_android.h b/src/xenia/ui/windowed_app_context_android.h new file mode 100644 index 000000000..08ddc31bc --- /dev/null +++ b/src/xenia/ui/windowed_app_context_android.h @@ -0,0 +1,61 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_WINDOWED_APP_CONTEXT_ANDROID_H_ +#define XENIA_UI_WINDOWED_APP_CONTEXT_ANDROID_H_ + +#include +#include + +#include "xenia/ui/windowed_app_context.h" + +namespace xe { +namespace ui { + +class WindowedApp; + +class AndroidWindowedAppContext final : public WindowedAppContext { + public: + // For calling from android.app.func_name exports. + static void StartAppOnNativeActivityCreate( + ANativeActivity* activity, void* saved_state, size_t saved_state_size, + std::unique_ptr (*app_creator)( + WindowedAppContext& app_context)); + + // Defined in the translation unit where WindowedApp is complete because of + // std::unique_ptr. + ~AndroidWindowedAppContext(); + + ANativeActivity* activity() const { return activity_; } + WindowedApp* app() const { return app_.get(); } + + void NotifyUILoopOfPendingFunctions() override; + + void PlatformQuitFromUIThread() override; + + private: + explicit AndroidWindowedAppContext(ANativeActivity* activity); + bool InitializeApp(std::unique_ptr (*app_creator)( + WindowedAppContext& app_context)); + + // TODO(Triang3l): Switch from ANativeActivity to the context itself being the + // object for communication with the Java code when NativeActivity isn't used + // anymore as its functionality is heavily limited. + ANativeActivity* activity_; + std::unique_ptr app_; + + // TODO(Triang3l): The rest of the context, including quit handler (and the + // destructor) calling `finish` on the activity, UI looper notification + // posting, etc. +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_WINDOWED_APP_CONTEXT_ANDROID_H_ diff --git a/src/xenia/ui/windowed_app_context_gtk.cc b/src/xenia/ui/windowed_app_context_gtk.cc new file mode 100644 index 000000000..ea092929c --- /dev/null +++ b/src/xenia/ui/windowed_app_context_gtk.cc @@ -0,0 +1,112 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/windowed_app_context_gtk.h" + +#include +#include +#include + +namespace xe { +namespace ui { + +GTKWindowedAppContext::~GTKWindowedAppContext() { + // Remove the idle sources as their data pointer (to this context) is now + // outdated. + if (quit_idle_pending_) { + g_source_remove(quit_idle_pending_); + } + { + // Lock the mutex for a pending_functions_idle_pending_ access memory + // barrier, even though no other threads can access this object anymore. + std::lock_guard pending_functions_idle_pending_lock( + pending_functions_idle_pending_mutex_); + if (pending_functions_idle_pending_) { + g_source_remove(pending_functions_idle_pending_); + } + } +} + +void GTKWindowedAppContext::NotifyUILoopOfPendingFunctions() { + std::lock_guard pending_functions_idle_pending_lock( + pending_functions_idle_pending_mutex_); + if (!pending_functions_idle_pending_) { + pending_functions_idle_pending_ = + gdk_threads_add_idle(PendingFunctionsSourceFunc, this); + } +} + +void GTKWindowedAppContext::PlatformQuitFromUIThread() { + if (quit_idle_pending_ || !gtk_main_level()) { + return; + } + gtk_main_quit(); + // Quit from all loops in the context, current inner, upcoming inner (this is + // why the idle function is added even at the main level of 1), until we can + // return to the tail of RunMainGTKLoop. + quit_idle_pending_ = gdk_threads_add_idle(QuitSourceFunc, this); +} + +void GTKWindowedAppContext::RunMainGTKLoop() { + // For safety, in case the quit request somehow happened before the loop. + if (HasQuitFromUIThread()) { + return; + } + assert_zero(gtk_main_level()); + gtk_main(); + // Something else - not QuitFromUIThread - might have done gtk_main_quit for + // the outer loop. If it has exited for some reason, let the context know, so + // pending function won't be added pointlessly. + QuitFromUIThread(); + // Don't interfere with main loops of other contexts that will possibly be + // executed in this thread later (if this is not the only app that will be + // executed in this process, for example, or if some WindowedAppContext-less + // dialog is opened after the windowed app has quit) - cancel the scheduled + // quit chain as we have quit already. + if (quit_idle_pending_) { + g_source_remove(quit_idle_pending_); + quit_idle_pending_ = 0; + } +} + +gboolean GTKWindowedAppContext::PendingFunctionsSourceFunc(gpointer data) { + auto& app_context = *static_cast(data); + // Allow new pending function notifications to be made, and stop tracking the + // lifetime of the idle source that is already being executed and thus + // removed. This must be done before executing the functions (if done after, + // notifications may be missed if they happen between the execution of the + // functions and the reset of pending_functions_idle_pending_). + { + std::lock_guard pending_functions_idle_pending_lock( + app_context.pending_functions_idle_pending_mutex_); + app_context.pending_functions_idle_pending_ = 0; + } + app_context.ExecutePendingFunctionsFromUIThread(); + return G_SOURCE_REMOVE; +} + +gboolean GTKWindowedAppContext::QuitSourceFunc(gpointer data) { + auto app_context = static_cast(data); + guint main_level = gtk_main_level(); + assert_not_zero(main_level); + if (!main_level) { + app_context->quit_idle_pending_ = 0; + return G_SOURCE_REMOVE; + } + gtk_main_quit(); + // Quit from all loops in the context, current inner, upcoming inner (this is + // why the idle function is added even at the main level of 1), until we can + // return to the tail of RunMainGTKLoop. + app_context->quit_idle_pending_ = + gdk_threads_add_idle(QuitSourceFunc, app_context); + return G_SOURCE_REMOVE; +} + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/windowed_app_context_gtk.h b/src/xenia/ui/windowed_app_context_gtk.h new file mode 100644 index 000000000..198e7a7a9 --- /dev/null +++ b/src/xenia/ui/windowed_app_context_gtk.h @@ -0,0 +1,46 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_WINDOWED_APP_CONTEXT_GTK_H_ +#define XENIA_UI_WINDOWED_APP_CONTEXT_GTK_H_ + +#include +#include + +#include "xenia/ui/windowed_app_context.h" + +namespace xe { +namespace ui { + +class GTKWindowedAppContext final : public WindowedAppContext { + public: + GTKWindowedAppContext() = default; + ~GTKWindowedAppContext(); + + void NotifyUILoopOfPendingFunctions() override; + + void PlatformQuitFromUIThread() override; + + void RunMainGTKLoop(); + + private: + static gboolean PendingFunctionsSourceFunc(gpointer data); + + static gboolean QuitSourceFunc(gpointer data); + + std::mutex pending_functions_idle_pending_mutex_; + guint pending_functions_idle_pending_ = 0; + + guint quit_idle_pending_ = 0; +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_WINDOWED_APP_CONTEXT_GTK_H_ diff --git a/src/xenia/ui/windowed_app_context_win.cc b/src/xenia/ui/windowed_app_context_win.cc new file mode 100644 index 000000000..399a2afd4 --- /dev/null +++ b/src/xenia/ui/windowed_app_context_win.cc @@ -0,0 +1,130 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/windowed_app_context_win.h" + +#include + +#include "xenia/base/platform_win.h" + +namespace xe { +namespace ui { + +bool Win32WindowedAppContext::pending_functions_window_class_registered_; + +Win32WindowedAppContext::~Win32WindowedAppContext() { + if (pending_functions_hwnd_) { + DestroyWindow(pending_functions_hwnd_); + } +} + +bool Win32WindowedAppContext::Initialize() { + // Logging possibly not initialized in this function yet. + // Create the message-only window for executing pending functions - using a + // window instead of executing them between iterations so non-main message + // loops, such as Windows modals, can execute pending functions too. + static const WCHAR kPendingFunctionsWindowClassName[] = + L"XeniaPendingFunctionsWindowClass"; + if (!pending_functions_window_class_registered_) { + WNDCLASSEXW pending_functions_window_class = {}; + pending_functions_window_class.cbSize = + sizeof(pending_functions_window_class); + pending_functions_window_class.lpfnWndProc = PendingFunctionsWndProc; + pending_functions_window_class.hInstance = hinstance_; + pending_functions_window_class.lpszClassName = + kPendingFunctionsWindowClassName; + if (!RegisterClassExW(&pending_functions_window_class)) { + return false; + } + pending_functions_window_class_registered_ = true; + } + pending_functions_hwnd_ = CreateWindowExW( + 0, kPendingFunctionsWindowClassName, L"Xenia Pending Functions", + WS_OVERLAPPED, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, + HWND_MESSAGE, nullptr, hinstance_, this); + if (!pending_functions_hwnd_) { + return false; + } + return true; +} + +void Win32WindowedAppContext::NotifyUILoopOfPendingFunctions() { + while (!PostMessageW(pending_functions_hwnd_, + kPendingFunctionsWindowClassMessageExecute, 0, 0)) { + Sleep(1); + } +} + +void Win32WindowedAppContext::PlatformQuitFromUIThread() { + // Send WM_QUIT to whichever loop happens to process it - may be the loop of a + // built-in modal window, which is unaware of HasQuitFromUIThread, don't let + // it delay quitting indefinitely. + PostQuitMessage(EXIT_SUCCESS); +} + +int Win32WindowedAppContext::RunMainMessageLoop() { + int result = EXIT_SUCCESS; + MSG message; + // The HasQuitFromUIThread check is not absolutely required, but for + // additional safety in case WM_QUIT is not received for any reason. + while (!HasQuitFromUIThread()) { + BOOL message_result = GetMessageW(&message, nullptr, 0, 0); + if (message_result == 0 || message_result == -1) { + // WM_QUIT (0 - this is the primary message loop, no need to resend, also + // contains the result from PostQuitMessage in wParam) or an error + // (-1). Quitting the context will run the pending functions. Getting + // WM_QUIT doesn't imply that QuitFromUIThread has been called already, it + // may originate in some place other than PlatformQuitFromUIThread as + // well - call it to finish everything including the pending functions. + QuitFromUIThread(); + result = message_result ? EXIT_FAILURE : int(message.wParam); + break; + } + TranslateMessage(&message); + DispatchMessageW(&message); + } + return result; +} + +LRESULT CALLBACK Win32WindowedAppContext::PendingFunctionsWndProc( + HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + if (message == WM_CLOSE) { + // Need the window for the entire context's lifetime, don't allow anything + // to close it. + return 0; + } + if (message == WM_NCCREATE) { + SetWindowLongPtrW( + hwnd, GWLP_USERDATA, + reinterpret_cast( + reinterpret_cast(lparam)->lpCreateParams)); + } else { + auto app_context = reinterpret_cast( + GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + if (app_context) { + switch (message) { + case WM_DESTROY: + // The message-only window owned by the context is being destroyed, + // thus the context won't be able to execute pending functions + // anymore - can't continue functioning normally. + app_context->QuitFromUIThread(); + break; + case kPendingFunctionsWindowClassMessageExecute: + app_context->ExecutePendingFunctionsFromUIThread(); + break; + default: + break; + } + } + } + return DefWindowProcW(hwnd, message, wparam, lparam); +} + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/windowed_app_context_win.h b/src/xenia/ui/windowed_app_context_win.h new file mode 100644 index 000000000..1c01961ac --- /dev/null +++ b/src/xenia/ui/windowed_app_context_win.h @@ -0,0 +1,58 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_WINDOWED_APP_CONTEXT_WIN_H_ +#define XENIA_UI_WINDOWED_APP_CONTEXT_WIN_H_ + +#include + +#include "xenia/base/platform_win.h" +#include "xenia/ui/windowed_app_context.h" + +namespace xe { +namespace ui { + +class Win32WindowedAppContext final : public WindowedAppContext { + public: + // Must call Initialize and check its result after creating to be able to + // perform pending function calls. + explicit Win32WindowedAppContext(HINSTANCE hinstance, int show_cmd) + : hinstance_(hinstance), show_cmd_(show_cmd) {} + ~Win32WindowedAppContext(); + + bool Initialize(); + + HINSTANCE hinstance() const { return hinstance_; } + int show_cmd() const { return show_cmd_; } + + void NotifyUILoopOfPendingFunctions() override; + + void PlatformQuitFromUIThread() override; + + int RunMainMessageLoop(); + + private: + enum : UINT { + kPendingFunctionsWindowClassMessageExecute = WM_USER, + }; + + static LRESULT CALLBACK PendingFunctionsWndProc(HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam); + + HINSTANCE hinstance_; + int show_cmd_; + HWND pending_functions_hwnd_ = nullptr; + + static bool pending_functions_window_class_registered_; +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_WINDOWED_APP_CONTEXT_WIN_H_ diff --git a/src/xenia/ui/windowed_app_main_posix.cc b/src/xenia/ui/windowed_app_main_posix.cc new file mode 100644 index 000000000..60b93a04e --- /dev/null +++ b/src/xenia/ui/windowed_app_main_posix.cc @@ -0,0 +1,64 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include +#include +#include +#include + +#include "xenia/base/cvar.h" +#include "xenia/base/logging.h" +#include "xenia/ui/windowed_app.h" +#include "xenia/ui/windowed_app_context_gtk.h" + +extern "C" int main(int argc_pre_gtk, char** argv_pre_gtk) { + // Initialize GTK+, which will handle and remove its own arguments from argv. + // Both GTK+ and Xenia use --option=value argument format (see man + // gtk-options), however, it's meaningless to try to parse the same argument + // both as a GTK+ one and as a cvar. Make GTK+ options take precedence in case + // of a name collision, as there's an alternative way of setting Xenia options + // (the config). + int argc_post_gtk = argc_pre_gtk; + char** argv_post_gtk = argv_pre_gtk; + if (!gtk_init_check(&argc_post_gtk, &argv_post_gtk)) { + // Logging has not been initialized yet. + std::fputs("Failed to initialize GTK+\n", stderr); + return EXIT_FAILURE; + } + + int result; + + { + xe::ui::GTKWindowedAppContext app_context; + + std::unique_ptr app = + xe::ui::GetWindowedAppCreator()(app_context); + + cvar::ParseLaunchArguments(argc_post_gtk, argv_post_gtk, + app->GetPositionalOptionsUsage(), + app->GetPositionalOptions()); + + // Initialize logging. Needs parsed cvars. + xe::InitializeLogging(app->GetName()); + + if (app->OnInitialize()) { + app_context.RunMainGTKLoop(); + result = EXIT_SUCCESS; + } else { + result = EXIT_FAILURE; + } + + app->InvokeOnDestroy(); + } + + // Logging may still be needed in the destructors. + xe::ShutdownLogging(); + + return result; +} diff --git a/src/xenia/ui/windowed_app_main_win.cc b/src/xenia/ui/windowed_app_main_win.cc new file mode 100644 index 000000000..114d36fc0 --- /dev/null +++ b/src/xenia/ui/windowed_app_main_win.cc @@ -0,0 +1,70 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include +#include + +#include "xenia/base/console.h" +#include "xenia/base/cvar.h" +#include "xenia/base/main_win.h" +#include "xenia/base/platform_win.h" +#include "xenia/ui/windowed_app.h" +#include "xenia/ui/windowed_app_context_win.h" + +DEFINE_bool(enable_console, false, "Open a console window with the main window", + "General"); + +int WINAPI wWinMain(HINSTANCE hinstance, HINSTANCE hinstance_prev, + LPWSTR command_line, int show_cmd) { + int result; + + { + xe::ui::Win32WindowedAppContext app_context(hinstance, show_cmd); + // TODO(Triang3l): Initialize creates a window. Set DPI awareness via the + // manifest. + if (!app_context.Initialize()) { + return EXIT_FAILURE; + } + + std::unique_ptr app = + xe::ui::GetWindowedAppCreator()(app_context); + + if (!xe::ParseWin32LaunchArguments(false, app->GetPositionalOptionsUsage(), + app->GetPositionalOptions(), nullptr)) { + return EXIT_FAILURE; + } + + // TODO(Triang3l): Rework this, need to initialize the console properly, + // disable has_console_attached_ by default in windowed apps, and attach + // only if needed. + if (cvars::enable_console) { + xe::AttachConsole(); + } + + // Initialize COM on the UI thread with the apartment-threaded concurrency + // model, so dialogs can be used. + if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED))) { + return EXIT_FAILURE; + } + + xe::InitializeWin32App(app->GetName()); + + result = + app->OnInitialize() ? app_context.RunMainMessageLoop() : EXIT_FAILURE; + + app->InvokeOnDestroy(); + } + + // Logging may still be needed in the destructors. + xe::ShutdownWin32App(); + + CoUninitialize(); + + return result; +} diff --git a/src/xenia/vfs/premake5.lua b/src/xenia/vfs/premake5.lua index 94b666dce..f312d93c6 100644 --- a/src/xenia/vfs/premake5.lua +++ b/src/xenia/vfs/premake5.lua @@ -27,7 +27,7 @@ project("xenia-vfs-dump") files({ "vfs_dump.cc", - project_root.."/src/xenia/base/main_"..platform_suffix..".cc", + project_root.."/src/xenia/base/console_app_main_"..platform_suffix..".cc", }) resincludedirs({ project_root, diff --git a/src/xenia/vfs/vfs_dump.cc b/src/xenia/vfs/vfs_dump.cc index 98d8eb1b7..2bca814bb 100644 --- a/src/xenia/vfs/vfs_dump.cc +++ b/src/xenia/vfs/vfs_dump.cc @@ -2,7 +2,7 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2020 Ben Vanik. All rights reserved. * + * Copyright 2021 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ @@ -11,8 +11,9 @@ #include #include +#include "xenia/base/console_app_main.h" +#include "xenia/base/cvar.h" #include "xenia/base/logging.h" -#include "xenia/base/main.h" #include "xenia/base/math.h" #include "xenia/vfs/devices/stfs_container_device.h" @@ -113,5 +114,5 @@ int vfs_dump_main(const std::vector& args) { } // namespace vfs } // namespace xe -DEFINE_ENTRY_POINT("xenia-vfs-dump", xe::vfs::vfs_dump_main, - "[source] [dump_path]", "source", "dump_path"); +XE_DEFINE_CONSOLE_APP("xenia-vfs-dump", xe::vfs::vfs_dump_main, + "[source] [dump_path]", "source", "dump_path"); diff --git a/tools/build/scripts/test_suite.lua b/tools/build/scripts/test_suite.lua index 563624c09..a83e891de 100644 --- a/tools/build/scripts/test_suite.lua +++ b/tools/build/scripts/test_suite.lua @@ -31,7 +31,7 @@ local function combined_test_suite(test_suite_name, project_root, base_path, con }) files({ project_root.."/"..build_tools_src.."/test_suite_main.cc", - project_root.."/src/xenia/base/main_"..platform_suffix..".cc", + project_root.."/src/xenia/base/console_app_main_"..platform_suffix..".cc", base_path.."/**_test.cc", }) filter("toolset:msc") diff --git a/tools/build/src/test_suite_main.cc b/tools/build/src/test_suite_main.cc index 1ea73ac36..1e34b4c8f 100644 --- a/tools/build/src/test_suite_main.cc +++ b/tools/build/src/test_suite_main.cc @@ -13,8 +13,8 @@ #include #include +#include "xenia/base/console_app_main.h" #include "xenia/base/cvar.h" -#include "xenia/base/main.h" #define CATCH_CONFIG_RUNNER #include "third_party/catch/single_include/catch2/catch.hpp" @@ -43,5 +43,5 @@ int test_suite_main(const std::vector& args) { #error XE_TEST_SUITE_NAME is undefined! #endif -DEFINE_ENTRY_POINT_TRANSPARENT(XE_TEST_SUITE_NAME, - xe::test_suite::test_suite_main); +XE_DEFINE_CONSOLE_APP_TRANSPARENT(XE_TEST_SUITE_NAME, + xe::test_suite::test_suite_main);