diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e20dd4414..ad5f7f3cf 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -34,6 +34,63 @@ up later, so be sure to check it out. Basically: run `xb format` before you add a commit and you won't have a problem. +## Referencing Sources + +In code interacting with guest interfaces or protocols, where applicable, please +leave comments describing how the information included in it was obtained. For +code based on analysis of the response of the original Xbox 360 hardware or +software, please provide reproduction information, such as an outline of the +algorithm executed, arguments and results of function calls or processor +instructions involved, GPU or XMA commands and state register changes. Having +traceable sources helps solve multiple issues: + +* The legality of the submitted code can be verified easily. +* Additional analysis based on reproduction steps from prior research can be + done to discover more details or to research the behavior of other related + features. +* The accuracy and completeness of the information can be evaluated. Knowing + whether something is ground truth about the console's behavior or an + approximation (for example, based on similar functionality in Windows, the + Qualcomm Adreno 200 GPU, AMD's desktop GPU architectures; the Direct3D 11.3 + functional specification, which may be used as a generic fallback when no + information specific to the Xenos or Direct3D 9 is available) may help avoid + redoing work that has already been done if the findings are accurate, or + making potentially wrong conclusions about related functionality if there's no + certainty about the correctness of the information. In addition, it's useful + to describe how complete the implementation of a feature is — such as edge + cases checked and covered. If you are unsure if your code accurately reflects + the behavior of the console, or you have deliberately made deviations due to + the lack of prerequisites for an accurate implementation or for performance + reasons (in case of the latter, it's recommended to provide both options, + selectable with a configuration variable), or you just want more research to + be done in the future, please leave a TODO comment in the format provided in + [style_guide.md](../docs/style_guide.md). + +If you have verified your code by checking the correctness of the behavior of a +game, **do not** refer to it by its title trademark. To avoid unnecessary +dependencies on third parties, instead, use the hexadecimal title ID number +displayed in the title bar beside the name of the game. It's also recommended to +avoid using proper names of game content if they can be replaced with easily +understandable pointers not including them, such as "first mission", +"protagonist", "enemy aircraft". + +Do not leave any hard-coded references to specific games, even in title ID form, +in any part of the user interface, including the configuration file. If you want +to provide an example of a game where changing a configuration variable may have +a noticeable effect, use a code comment near the declaration of the variable +rather than its description string. Any game identifiers referenced in the user +interface must be obtained only from information provided by the user such as +game data files. + +Also, do not put any conditionals based on hard-coded identifiers of games — the +task of the project is researching the Xbox 360 console itself and documenting +its behavior by creating open implementations of its interfaces. Game-specific +hacks provide no help in achieving that, instead only complicating research by +introducing incorrect state and hiding the symptoms of actual issues. While +temporary workarounds, though discouraged, may be added in cases when progress +would be blocked otherwise in other areas, they must be expressed and reasoned +in terms of the common interface rather than logic internal to a specific game. + ## Clean Git History Tools such as `git bisect` are used on the repository regularly to check for and diff --git a/premake5.lua b/premake5.lua index 8ea9ce93c..3c2c9d49d 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") @@ -242,9 +248,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 658303906..156946750 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" @@ -40,12 +41,13 @@ using xe::ui::MenuItem; using xe::ui::MouseEvent; using xe::ui::UIEvent; -const std::string kBaseTitle = "xenia"; +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 @@ -54,27 +56,23 @@ EmulatorWindow::EmulatorWindow(Emulator* emulator) " CHECKED" #endif #endif - " (" XE_BUILD_BRANCH "/" XE_BUILD_COMMIT_SHORT "/" XE_BUILD_DATE + " (" +#ifdef XE_BUILD_IS_PR + "PR#" XE_BUILD_PR_NUMBER " " XE_BUILD_PR_REPO + " " XE_BUILD_PR_BRANCH "@" XE_BUILD_PR_COMMIT_SHORT " against " +#endif + XE_BUILD_BRANCH "@" XE_BUILD_COMMIT_SHORT " on " XE_BUILD_DATE ")"; } -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; } @@ -86,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()); }); @@ -308,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" }, @@ -426,8 +424,13 @@ void EmulatorWindow::ToggleFullscreen() { void EmulatorWindow::ShowHelpWebsite() { LaunchWebBrowser("https://xenia.jp"); } void EmulatorWindow::ShowCommitID() { +#ifdef XE_BUILD_IS_PR + LaunchWebBrowser( + "https://github.com/xenia-project/xenia/pull/" XE_BUILD_PR_NUMBER); +#else LaunchWebBrowser( "https://github.com/xenia-project/xenia/commit/" XE_BUILD_COMMIT "/"); +#endif } void EmulatorWindow::UpdateTitle() { 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 fed5c7e82..31aa50d79 100644 --- a/src/xenia/app/premake5.lua +++ b/src/xenia/app/premake5.lua @@ -50,8 +50,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({ @@ -71,7 +71,6 @@ project("xenia-app") "X11", "xcb", "X11-xcb", - "vulkan", "SDL2", }) diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc index e3f8b9fbc..fd02de80a 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; factory.Add("vulkan"); // TODO(Triang3l): Move D3D12 back to the top. @@ -188,7 +249,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) { @@ -216,9 +277,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; @@ -276,20 +337,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) { @@ -298,10 +394,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"); } } } @@ -312,10 +409,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"); } } @@ -324,79 +421,78 @@ 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"); + } + } + + // Some (older?) games try accessing cache:\ too + // NOTE: this must be registered _after_ the cache0/cache1 devices, due to + // substring/start_with logic inside VirtualFileSystem::ResolvePath, else + // accesses to those devices will go here instead + auto cache_device = + std::make_unique("\\CACHE", "cache", false); + if (!cache_device->Initialize()) { + XELOGE("Unable to scan cache path"); + } else { + if (!emulator_->file_system()->RegisterDevice(std::move(cache_device))) { + XELOGE("Unable to register cache path"); + } else { + 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; @@ -405,50 +501,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/apu/xma_context.cc b/src/xenia/apu/xma_context.cc index 9791cba85..a9662a0d8 100644 --- a/src/xenia/apu/xma_context.cc +++ b/src/xenia/apu/xma_context.cc @@ -811,12 +811,13 @@ void XmaContext::ConvertFrame(const uint8_t** samples, bool is_two_channel, // Loop through every sample, convert and drop it into the output array. // If more than one channel, we need to interleave the samples from each // channel next to each other. Always saturate because FFmpeg output is - // not limited to [-1, 1] (for example 1.095 as seen in RDR) + // not limited to [-1, 1] (for example 1.095 as seen in 5454082B). constexpr float scale = (1 << 15) - 1; auto out = reinterpret_cast(output_buffer); - // For testing of vectorized versions, stereo audio is common in Halo 3, since - // the first menu frame; the intro cutscene also has more than 2 channels. + // For testing of vectorized versions, stereo audio is common in 4D5307E6, + // since the first menu frame; the intro cutscene also has more than 2 + // channels. #if XE_ARCH_AMD64 static_assert(kSamplesPerFrame % 8 == 0); const auto in_channel_0 = reinterpret_cast(samples[0]); 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/cvar.h b/src/xenia/base/cvar.h index 6047b1bd8..1f92d0766 100644 --- a/src/xenia/base/cvar.h +++ b/src/xenia/base/cvar.h @@ -15,8 +15,9 @@ #include #include -#include "cpptoml/include/cpptoml.h" -#include "cxxopts/include/cxxopts.hpp" +#include "third_party/cpptoml/include/cpptoml.h" +#include "third_party/cxxopts/include/cxxopts.hpp" +#include "third_party/fmt/include/fmt/format.h" #include "xenia/base/assert.h" #include "xenia/base/filesystem.h" #include "xenia/base/string_util.h" @@ -216,7 +217,10 @@ inline std::string CommandVar::ToString( template std::string CommandVar::ToString(T val) { - return std::to_string(val); + // Use fmt::format instead of std::to_string for locale-neutral formatting of + // floats, always with a period rather than a comma, which is treated as an + // unidentified trailing character by cpptoml. + return fmt::format("{}", val); } template 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 6cbf8c9ae..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,89 +75,42 @@ 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("Build: " XE_BUILD_BRANCH " / " XE_BUILD_COMMIT " on " XE_BUILD_DATE); + XELOGI( + "Build: " +#ifdef XE_BUILD_IS_PR + "PR#" XE_BUILD_PR_NUMBER " " XE_BUILD_PR_REPO " " XE_BUILD_PR_BRANCH + "@" XE_BUILD_PR_COMMIT_SHORT " against " +#endif + XE_BUILD_BRANCH "@" XE_BUILD_COMMIT_SHORT " on " XE_BUILD_DATE); // Request high performance timing. if (cvars::win32_high_freq) { 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/base/string_util.h b/src/xenia/base/string_util.h index 59d6e90f6..3c73f9c65 100644 --- a/src/xenia/base/string_util.h +++ b/src/xenia/base/string_util.h @@ -221,7 +221,7 @@ inline T fpfs(const std::string_view value, bool force_hex) { } else { #if XE_COMPILER_CLANG || XE_COMPILER_GNUC auto temp = std::string(range); - result = std::strtof(temp.c_str(), nullptr); + result = std::strtod(temp.c_str(), nullptr); #else auto [p, error] = std::from_chars(range.data(), range.data() + range.size(), result, std::chars_format::general); diff --git a/src/xenia/cpu/backend/x64/x64_seq_vector.cc b/src/xenia/cpu/backend/x64/x64_seq_vector.cc index 4c7fb665a..b5a8e49a2 100644 --- a/src/xenia/cpu/backend/x64/x64_seq_vector.cc +++ b/src/xenia/cpu/backend/x64/x64_seq_vector.cc @@ -1862,8 +1862,8 @@ struct PACK : Sequence> { src = i.src1; } // Saturate to [3,3....] so that only values between 3...[00] and 3...[FF] - // are valid - max before min to pack NaN as zero (Red Dead Redemption is - // heavily affected by the order - packs 0xFFFFFFFF in matrix code to get 0 + // are valid - max before min to pack NaN as zero (5454082B is heavily + // affected by the order - packs 0xFFFFFFFF in matrix code to get a 0 // constant). e.vmaxps(i.dest, src, e.GetXmmConstPtr(XMM3333)); e.vminps(i.dest, i.dest, e.GetXmmConstPtr(XMMPackD3DCOLORSat)); diff --git a/src/xenia/cpu/ppc/ppc_emit_altivec.cc b/src/xenia/cpu/ppc/ppc_emit_altivec.cc index 770def3c1..93f4df2b7 100644 --- a/src/xenia/cpu/ppc/ppc_emit_altivec.cc +++ b/src/xenia/cpu/ppc/ppc_emit_altivec.cc @@ -2069,7 +2069,8 @@ int InstrEmit_vpkd3d128(PPCHIRBuilder& f, const InstrData& i) { v = f.Pack(v, PACK_TYPE_FLOAT16_4); break; case 6: // VPACK_NORMPACKED64 4_20_20_20 w_z_y_x - // Used in 2K games like NBA 2K9, pretty rarely in general. + // Used in 54540829 and other installments in the series, pretty rarely in + // general. v = f.Pack(v, PACK_TYPE_ULONG_4202020); break; default: 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 3020c170c..016952351 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 36a56c946..7bb4109e4 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -40,8 +40,11 @@ #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" #include "xenia/vfs/devices/stfs_container_device.h" #include "xenia/vfs/virtual_file_system.h" @@ -232,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_); }); @@ -279,7 +282,7 @@ X_STATUS Emulator::LaunchXexFile(const std::filesystem::path& path) { // and then get that symlinked to game:\, so // -> game:\foo.xex - auto mount_path = "\\Device\\Harddisk0\\Partition0"; + auto mount_path = "\\Device\\Harddisk0\\Partition1"; // Register the local directory in the virtual filesystem. auto parent_path = path.parent_path(); @@ -582,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" @@ -672,6 +675,25 @@ static std::string format_version(xex2_version version) { X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, const std::string_view module_path) { + // Setup NullDevices for raw HDD partition accesses + // Cache/STFC code baked into games tries reading/writing to these + // By using a NullDevice that just returns success to all IO requests it + // should allow games to believe cache/raw disk was accessed successfully + + // NOTE: this should probably be moved to xenia_main.cc, but right now we need + // to register the \Device\Harddisk0\ NullDevice _after_ the + // \Device\Harddisk0\Partition1 HostPathDevice, otherwise requests to + // Partition1 will go to this. Registering during CompleteLaunch allows us to + // make sure any HostPathDevices are ready beforehand. + // (see comment above cache:\ device registration for more info about why) + auto null_paths = {std::string("\\Partition0"), std::string("\\Cache0"), + std::string("\\Cache1")}; + auto null_device = + std::make_unique("\\Device\\Harddisk0", null_paths); + if (null_device->Initialize()) { + file_system_->RegisterDevice(std::move(null_device)); + } + // Reset state. title_id_ = std::nullopt; title_name_ = ""; diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 53af72115..2e1a43c36 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -738,7 +738,7 @@ bool CommandProcessor::ExecutePacketType3(RingBuffer* reader, uint32_t packet) { break; } case PM4_WAIT_FOR_IDLE: { - // This opcode is used by "Duke Nukem Forever" while going/being ingame + // This opcode is used by 5454084E while going / being ingame. assert_true(count == 1); uint32_t value = reader->ReadAndSwap(); XELOGGPU("GPU wait for idle = {:08X}", value); @@ -1168,7 +1168,7 @@ bool CommandProcessor::ExecutePacketType3_EVENT_WRITE_ZPD(RingBuffer* reader, // and used to detect a finished query. bool is_end_via_z_pass = pSampleCounts->ZPass_A == kQueryFinished && pSampleCounts->ZPass_B == kQueryFinished; - // Older versions of D3D also checks for ZFail (First Gears of War) + // Older versions of D3D also checks for ZFail (4D5307D5). bool is_end_via_z_fail = pSampleCounts->ZFail_A == kQueryFinished && pSampleCounts->ZFail_B == kQueryFinished; std::memset(pSampleCounts, 0, sizeof(xe_gpu_depth_sample_counts)); diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.cc b/src/xenia/gpu/d3d12/d3d12_command_processor.cc index fca476673..39bdfb664 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.cc +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.cc @@ -1662,7 +1662,7 @@ void D3D12CommandProcessor::PerformSwap(uint32_t frontbuffer_ptr, gamma_ramp_upload_mapping_ + gamma_ramp_footprint.Offset); for (uint32_t i = 0; i < 256; ++i) { uint32_t value = gamma_ramp_.normal[i].value; - // Swap red and blue (Project Sylpheed has settings allowing separate + // Swap red and blue (535107D4 has settings allowing separate // configuration). mapping[i] = ((value & 1023) << 20) | (value & (1023 << 10)) | ((value >> 20) & 1023); @@ -2076,7 +2076,7 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, memexport_stream.index_count * memexport_format_size; // Try to reduce the number of shared memory operations when writing // different elements into the same buffer through different exports - // (happens in Halo 3). + // (happens in 4D5307E6). bool memexport_range_reused = false; for (uint32_t i = 0; i < memexport_range_count; ++i) { MemExportRange& memexport_range = memexport_ranges[i]; @@ -2878,8 +2878,9 @@ void D3D12CommandProcessor::UpdateSystemConstantValues( // Get the color info register values for each render target. Also, for ROV, // exclude components that don't exist in the format from the write mask. // Don't exclude fully overlapping render targets, however - two render - // targets with the same base address are used in the lighting pass of Halo 3, - // for example, with the needed one picked with dynamic control flow. + // targets with the same base address are used in the lighting pass of + // 4D5307E6, for example, with the needed one picked with dynamic control + // flow. reg::RB_COLOR_INFO color_infos[4]; float rt_clamp[4][4]; uint32_t rt_keep_masks[4][2]; @@ -2898,8 +2899,8 @@ void D3D12CommandProcessor::UpdateSystemConstantValues( } // Disable depth and stencil if it aliases a color render target (for - // instance, during the XBLA logo in Banjo-Kazooie, though depth writing is - // already disabled there). + // instance, during the XBLA logo in 58410954, though depth writing is already + // disabled there). bool depth_stencil_enabled = rb_depthcontrol.stencil_enable || rb_depthcontrol.z_enable; if (edram_rov_used && depth_stencil_enabled) { diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.h b/src/xenia/gpu/d3d12/d3d12_command_processor.h index 018c568d0..e4d810c41 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.h +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.h @@ -83,9 +83,9 @@ class D3D12CommandProcessor : public CommandProcessor { // Gets the current color write mask, taking the pixel shader's write mask // into account. If a shader doesn't write to a render target, it shouldn't be - // written to and it shouldn't be even bound - otherwise, in Halo 3, one + // written to and it shouldn't be even bound - otherwise, in 4D5307E6, one // render target is being destroyed by a shader not writing anything, and in - // Banjo-Tooie, the result of clearing the top tile is being ignored because + // 58410955, the result of clearing the top tile is being ignored because // there are 4 render targets bound with the same EDRAM base (clearly not // correct usage), but the shader only clears 1, and then EDRAM buffer stores // conflict with each other. 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_render_target_cache.cc b/src/xenia/gpu/d3d12/d3d12_render_target_cache.cc index 53df32683..1437e696a 100644 --- a/src/xenia/gpu/d3d12/d3d12_render_target_cache.cc +++ b/src/xenia/gpu/d3d12/d3d12_render_target_cache.cc @@ -3619,7 +3619,7 @@ D3D12RenderTargetCache::GetOrCreateTransferPipelines(TransferShaderKey key) { case xenos::DepthRenderTargetFormat::kD24S8: // Round to the nearest even integer. This seems to be the correct, // adding +0.5 and rounding towards zero results in red instead of - // black in GTA IV and Halo 3 clear shaders. + // black in the 4D5307E6 clear shader. a.OpMul(dxbc::Dest::R(i, 0b1000), dxbc::Src::R(i, dxbc::Src::kWWWW), dxbc::Src::LF(float(0xFFFFFF))); a.OpRoundNE(dxbc::Dest::R(i, 0b1000), @@ -3804,7 +3804,7 @@ D3D12RenderTargetCache::GetOrCreateTransferPipelines(TransferShaderKey key) { case xenos::DepthRenderTargetFormat::kD24S8: // Round to the nearest even integer. This seems to be the correct, // adding +0.5 and rounding towards zero results in red instead of - // black in GTA IV and Halo 3 clear shaders. + // black in the 4D5307E6 clear shader. a.OpMul(dxbc::Dest::R(1, 0b1000), dxbc::Src::R(1, dxbc::Src::kWWWW), dxbc::Src::LF(float(0xFFFFFF))); a.OpRoundNE(dxbc::Dest::R(1, 0b1000), @@ -4181,7 +4181,7 @@ D3D12RenderTargetCache::GetOrCreateTransferPipelines(TransferShaderKey key) { case xenos::DepthRenderTargetFormat::kD24S8: // Round to the nearest even integer. This seems to be the // correct, adding +0.5 and rounding towards zero results in red - // instead of black in GTA IV and Halo 3 clear shaders. + // instead of black in the 4D5307E6 clear shader. a.OpMul(dxbc::Dest::R(0, 0b0010), dxbc::Src::R(0, dxbc::Src::kXXXX), dxbc::Src::LF(float(0xFFFFFF))); @@ -6228,7 +6228,7 @@ ID3D12PipelineState* D3D12RenderTargetCache::GetOrCreateDumpPipeline( case xenos::DepthRenderTargetFormat::kD24S8: // Round to the nearest even integer. This seems to be the correct, // adding +0.5 and rounding towards zero results in red instead of - // black in GTA IV and Halo 3 clear shaders. + // black in the 4D5307E6 clear shader. a.OpMul(dxbc::Dest::R(1, 0b0001), dxbc::Src::R(1, dxbc::Src::kXXXX), dxbc::Src::LF(float(0xFFFFFF))); a.OpRoundNE(dxbc::Dest::R(1, 0b0001), 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/pipeline_cache.cc b/src/xenia/gpu/d3d12/pipeline_cache.cc index 556c7cacf..879bc8eee 100644 --- a/src/xenia/gpu/d3d12/pipeline_cache.cc +++ b/src/xenia/gpu/d3d12/pipeline_cache.cc @@ -1567,7 +1567,8 @@ bool PipelineCache::GetCurrentStateDescription( /* 16 */ PipelineBlendFactor::kSrcAlphaSat, }; // Like kBlendFactorMap, but with color modes changed to alpha. Some - // pipelines aren't created in Prey because a color mode is used for alpha. + // pipelines aren't created in 545407E0 because a color mode is used for + // alpha. static const PipelineBlendFactor kBlendFactorAlphaMap[32] = { /* 0 */ PipelineBlendFactor::kZero, /* 1 */ PipelineBlendFactor::kOne, @@ -1599,7 +1600,7 @@ bool PipelineCache::GetCurrentStateDescription( // have their sample count matching the one set in the pipeline - however if // we set NumRenderTargets to 0 and also disable depth / stencil, the sample // count must be set to 1 - while the command list may still have - // multisampled render targets bound (happens in Halo 3 main menu). + // multisampled render targets bound (happens in 4D5307E6 main menu). // TODO(Triang3l): Investigate interaction of OMSetRenderTargets with // non-null depth and DSVFormat DXGI_FORMAT_UNKNOWN in the same case. for (uint32_t i = 0; i < 4; ++i) { @@ -2005,7 +2006,7 @@ ID3D12PipelineState* PipelineCache::CreateD3D12Pipeline( state_desc.BlendState.RenderTarget[i]; // Treat 1 * src + 0 * dest as disabled blending (there are opaque // surfaces drawn with blending enabled, but it's 1 * src + 0 * dest, in - // Call of Duty 4 - GPU performance is better when not blending. + // 415607E6 - GPU performance is better when not blending. if (rt.src_blend != PipelineBlendFactor::kOne || rt.dest_blend != PipelineBlendFactor::kZero || rt.blend_op != xenos::BlendOp::kAdd || 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/d3d12/texture_cache.cc b/src/xenia/gpu/d3d12/texture_cache.cc index 17244c573..8f7fc0fb9 100644 --- a/src/xenia/gpu/d3d12/texture_cache.cc +++ b/src/xenia/gpu/d3d12/texture_cache.cc @@ -121,8 +121,8 @@ namespace shaders { // components of operands in shaders. // For DXT3A and DXT5A, RRRR swizzle is specified in: // http://fileadmin.cs.lth.se/cs/Personal/Michael_Doggett/talks/unc-xenos-doggett.pdf -// Halo 3 also expects replicated components in k_8 sprites. -// DXN is read as RG in Halo 3, but as RA in Call of Duty. +// 4D5307E6 also expects replicated components in k_8 sprites. +// DXN is read as RG in 4D5307E6, but as RA in 415607E6. // TODO(Triang3l): Find out the correct contents of unused texture components. const TextureCache::HostFormat TextureCache::host_formats_[64] = { // k_1_REVERSE @@ -250,9 +250,9 @@ const TextureCache::HostFormat TextureCache::host_formats_[64] = { LoadMode::kUnknown, {2, 1, 0, 3}}, // k_Y1_Cr_Y0_Cb_REP - // Used for videos in NBA 2K9. Red and blue must be swapped. + // Used for videos in 54540829. Red and blue must be swapped. // TODO(Triang3l): D3DFMT_G8R8_G8B8 is DXGI_FORMAT_R8G8_B8G8_UNORM * 255.0f, - // watch out for num_format int, division in shaders, etc., in NBA 2K9 it + // watch out for num_format int, division in shaders, etc., in 54540829 it // works as is. Also need to decompress if the size is uneven, but should be // a very rare case. {DXGI_FORMAT_R8G8_B8G8_UNORM, @@ -1309,7 +1309,7 @@ void TextureCache::RequestTextures(uint32_t used_texture_mask) { // Clear the bindings not only for this draw call, but entirely, because // loading may be needed in some draw call later, which may have the same // key for some binding as before the invalidation, but texture_invalidated_ - // being false (menu background in Halo 3). + // being false (menu background in 4D5307E6). for (size_t i = 0; i < xe::countof(texture_bindings_); ++i) { texture_bindings_[i].Clear(); } diff --git a/src/xenia/gpu/d3d12/texture_cache.h b/src/xenia/gpu/d3d12/texture_cache.h index 5c9c049c4..21a100ea2 100644 --- a/src/xenia/gpu/d3d12/texture_cache.h +++ b/src/xenia/gpu/d3d12/texture_cache.h @@ -418,7 +418,7 @@ class TextureCache { // Uncompression info for when the regular host format for this texture is // block-compressed, but the size is not block-aligned, and thus such // texture cannot be created in Direct3D on PC and needs decompression, - // however, such textures are common, for instance, in Halo 3. This only + // however, such textures are common, for instance, in 4D5307E6. This only // supports unsigned normalized formats - let's hope GPUSIGN_SIGNED was not // used for DXN and DXT5A. DXGI_FORMAT dxgi_format_uncompressed; diff --git a/src/xenia/gpu/draw_util.cc b/src/xenia/gpu/draw_util.cc index 49149b7f9..5157c1a88 100644 --- a/src/xenia/gpu/draw_util.cc +++ b/src/xenia/gpu/draw_util.cc @@ -24,12 +24,13 @@ #include "xenia/gpu/texture_util.h" #include "xenia/gpu/xenos.h" +// Very prominent in 545407F2. DEFINE_bool( resolve_resolution_scale_duplicate_second_pixel, true, "When using resolution scale, apply the hack that duplicates the " "right/lower host pixel in the left and top sides of render target resolve " "areas to eliminate the gap caused by half-pixel offset (this is necessary " - "for certain games like GTA IV to work).", + "for certain games to display the scene graphics).", "GPU"); DEFINE_bool( @@ -952,11 +953,11 @@ bool GetResolveInfo(const RegisterFile& regs, const Memory& memory, dest_dimension = xenos::DataDimension::k2DOrStacked; // RB_COPY_DEST_PITCH::copy_dest_height is the real texture height used // for 3D texture pitch, it's not relative to 0,0 of the coordinate space - // (in Halo 3, the sniper rifle scope has copy_dest_height of 192, but the - // rectangle's Y is 64...256) - provide the real height of the rectangle - // since 32x32 tiles are stored linearly anyway. In addition, the height - // in RB_COPY_DEST_PITCH may be larger than needed - in Red Dead - // Redemption, a UI texture for the letterbox bars alpha is located within + // (in 4D5307E6, the sniper rifle scope has copy_dest_height of 192, but + // the rectangle's Y is 64...256) - provide the real height of the + // rectangle since 32x32 tiles are stored linearly anyway. In addition, + // the height in RB_COPY_DEST_PITCH may be larger than needed - in + // 5454082B, a UI texture for the letterbox bars alpha is located within // the range of a 1280x720 resolve target, so with resolution scaling it's // also wrongly detected as scaled, while only 1280x208 is being resolved. dest_height = uint32_t(y1 - y0); diff --git a/src/xenia/gpu/draw_util.h b/src/xenia/gpu/draw_util.h index d23d0d25a..8aa7287f6 100644 --- a/src/xenia/gpu/draw_util.h +++ b/src/xenia/gpu/draw_util.h @@ -67,7 +67,7 @@ constexpr bool IsPrimitivePolygonal(bool vgt_output_path_is_tessellation_enable, // TODO(Triang3l): Investigate how kRectangleList should be treated - possibly // actually drawn as two polygons on the console, however, the current // geometry shader doesn't care about the winding order - allowing backface - // culling for rectangles currently breaks Gears of War 2. + // culling for rectangles currently breaks 4D53082D. return false; } @@ -112,10 +112,10 @@ constexpr float GetD3D10PolygonOffsetFactor( return float(1 << 24); } // 20 explicit + 1 implicit (1.) mantissa bits. - // 2^20 is not enough for Call of Duty 4 retail version's first mission F.N.G. - // shooting range floor (with the number 1) on Direct3D 12. Tested on Nvidia - // GeForce GTX 1070, the exact formula (taking into account the 0...1 to - // 0...0.5 remapping described below) used for testing is + // 2^20 is not enough for 415607E6 retail version's training mission shooting + // range floor (with the number 1) on Direct3D 12. Tested on Nvidia GeForce + // GTX 1070, the exact formula (taking into account the 0...1 to 0...0.5 + // remapping described below) used for testing is // `int(ceil(offset * 2^20 * 0.5)) * sign(offset)`. With 2^20 * 0.5, there // are various kinds of stripes dependending on the view angle in that // location. With 2^21 * 0.5, the issue is not present. @@ -141,7 +141,7 @@ inline bool DoesCoverageDependOnAlpha(reg::RB_COLORCONTROL rb_colorcontrol) { // pre-passes and shadowmaps. The shader must have its ucode analyzed. If // IsRasterizationPotentiallyDone, this shouldn't be called, and assumed false // instead. Helps reject the pixel shader in some cases - memexport draws in -// Halo 3, and also most of some 1-point draws not covering anything done for +// 4D5307E6, and also most of some 1-point draws not covering anything done for // some reason in different games with a leftover pixel shader from the previous // draw, but with SQ_PROGRAM_CNTL destroyed, reducing the number of // unpredictable unneeded translations of random shaders with different host diff --git a/src/xenia/gpu/dxbc_shader_translator.cc b/src/xenia/gpu/dxbc_shader_translator.cc index 30582b022..c66ad535f 100644 --- a/src/xenia/gpu/dxbc_shader_translator.cc +++ b/src/xenia/gpu/dxbc_shader_translator.cc @@ -23,11 +23,12 @@ #include "xenia/gpu/xenos.h" #include "xenia/ui/graphics_provider.h" +// The test case for AMD is 4D5307E6 (checked in 2018). DEFINE_bool(dxbc_switch, true, "Use switch rather than if for flow control. Turning this off or " "on may improve stability, though this heavily depends on the " "driver - on AMD, it's recommended to have this set to true, as " - "Halo 3 appears to crash when if is used for flow control " + "some titles appear to crash when if is used for flow control " "(possibly the shader compiler tries to flatten them). On Intel " "HD Graphics, this is ignored because of a crash with the switch " "instruction.", @@ -398,7 +399,7 @@ void DxbcShaderTranslator::StartVertexOrDomainShader() { assert_true(register_count() >= 2); if (register_count() >= 1) { // Copy the domain location to r0.xyz. - // ZYX swizzle according to Call of Duty 3 and Viva Pinata. + // ZYX swizzle according to 415607E1 and 4D5307F2. in_domain_location_used_ |= 0b0111; a_.OpMov(uses_register_dynamic_addressing ? dxbc::Dest::X(0, 0, 0b0111) : dxbc::Dest::R(0, 0b0111), @@ -425,7 +426,7 @@ void DxbcShaderTranslator::StartVertexOrDomainShader() { if (register_count() >= 1) { // Copy the domain location to r0.xyz. // ZYX swizzle with r1.y == 0, according to the water shader in - // Banjo-Kazooie: Nuts & Bolts. + // 4D5307ED. in_domain_location_used_ |= 0b0111; a_.OpMov(uses_register_dynamic_addressing ? dxbc::Dest::X(0, 0, 0b0111) : dxbc::Dest::R(0, 0b0111), @@ -447,10 +448,10 @@ void DxbcShaderTranslator::StartVertexOrDomainShader() { // appears that the tessellator offloads the reordering of coordinates // for edges to game shaders. // - // In Banjo-Kazooie: Nuts & Bolts, the water shader multiplies the - // first control point's position by r0.z, the second CP's by r0.y, - // and the third CP's by r0.x. But before doing that it swizzles - // r0.xyz the following way depending on the value in r1.y: + // In 4D5307ED, the water shader multiplies the first control point's + // position by r0.z, the second CP's by r0.y, and the third CP's by + // r0.x. But before doing that it swizzles r0.xyz the following way + // depending on the value in r1.y: // - ZXY for 1.0. // - YZX for 2.0. // - XZY for 4.0. @@ -478,9 +479,9 @@ void DxbcShaderTranslator::StartVertexOrDomainShader() { a_.OpMov(uses_register_dynamic_addressing ? dxbc::Dest::X(0, 0, 0b0011) : dxbc::Dest::R(0, 0b0011), dxbc::Src::VDomain()); - // Control point indices according to the shader from the main menu of - // Defender, which starts from `cndeq r2, c255.xxxy, r1.xyzz, r0.zzzz`, - // where c255.x is 0, and c255.y is 1. + // Control point indices according the main menu of 58410823, with + // `cndeq r2, c255.xxxy, r1.xyzz, r0.zzzz` in the prologue of the + // shader, where c255.x is 0, and c255.y is 1. // r0.z for (1 - r0.x) * (1 - r0.y) // r1.x for r0.x * (1 - r0.y) // r1.y for r0.x * r0.y @@ -509,7 +510,7 @@ void DxbcShaderTranslator::StartVertexOrDomainShader() { assert_true(register_count() >= 2); if (register_count() >= 1) { // Copy the domain location to r0.yz. - // XY swizzle according to the ground shader in Viva Pinata. + // XY swizzle according to the ground shader in 4D5307F2. in_domain_location_used_ |= 0b0011; a_.OpMov(uses_register_dynamic_addressing ? dxbc::Dest::X(0, 0, 0b0110) : dxbc::Dest::R(0, 0b0110), @@ -530,9 +531,8 @@ void DxbcShaderTranslator::StartVertexOrDomainShader() { // the tessellator offloads the reordering of coordinates for edges to // game shaders. // - // In Viva Pinata, if we assume that r0.y is U and r0.z is V, the - // factors each control point value is multiplied by are the - // following: + // In 4D5307F2, if we assume that r0.y is U and r0.z is V, the factors + // each control point value is multiplied by are the following: // - (1-u)*(1-v), u*(1-v), (1-u)*v, u*v for 0.0 (identity swizzle). // - u*(1-v), (1-u)*(1-v), u*v, (1-u)*v for 1.0 (YXWZ). // - u*v, (1-u)*v, u*(1-v), (1-u)*(1-v) for 2.0 (WZYX). @@ -1452,7 +1452,7 @@ void DxbcShaderTranslator::StoreResult(const InstructionResult& result, dest = dxbc::Dest::R(system_temp_point_size_edge_flag_kill_vertex_); break; case InstructionStorageTarget::kExportAddress: - // Validate memexport writes (Halo 3 has some weird invalid ones). + // Validate memexport writes (4D5307E6 has some completely invalid ones). if (!can_store_memexport_address || memexport_alloc_current_count_ == 0 || memexport_alloc_current_count_ > Shader::kMaxMemExports || system_temps_memexport_address_[memexport_alloc_current_count_ - 1] == @@ -1463,7 +1463,7 @@ void DxbcShaderTranslator::StoreResult(const InstructionResult& result, system_temps_memexport_address_[memexport_alloc_current_count_ - 1]); break; case InstructionStorageTarget::kExportData: { - // Validate memexport writes (Halo 3 has some weird invalid ones). + // Validate memexport writes (4D5307E6 has some completely invalid ones). if (memexport_alloc_current_count_ == 0 || memexport_alloc_current_count_ > Shader::kMaxMemExports || system_temps_memexport_data_[memexport_alloc_current_count_ - 1] diff --git a/src/xenia/gpu/dxbc_shader_translator_fetch.cc b/src/xenia/gpu/dxbc_shader_translator_fetch.cc index cb2d529da..4033239e0 100644 --- a/src/xenia/gpu/dxbc_shader_translator_fetch.cc +++ b/src/xenia/gpu/dxbc_shader_translator_fetch.cc @@ -705,10 +705,10 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction( // Add a small epsilon to the offset (1.5/4 the fixed-point texture // coordinate ULP - shouldn't significantly effect the fixed-point // conversion; 1/4 is also not enough with 3x resolution scaling very - // noticeably on the weapon in Halo 3) to resolve ambiguity when fetching + // noticeably on the weapon in 4D5307E6) to resolve ambiguity when fetching // point-sampled textures between texels. This applies to both normalized - // (Banjo-Kazooie Xbox Live Arcade logo, coordinates interpolated between - // vertices with half-pixel offset) and unnormalized (Halo 3 lighting + // (58410954 Xbox Live Arcade logo, coordinates interpolated between + // vertices with half-pixel offset) and unnormalized (4D5307E6 lighting // G-buffer reading, ps_param_gen pixels) coordinates. On Nvidia Pascal, // without this adjustment, blockiness is visible in both cases. Possibly // there is a better way, however, an attempt was made to error-correct @@ -1595,13 +1595,12 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction( // - Data. - // Viva Pinata uses vertex displacement map textures for tessellated - // models like the beehive tree with explicit LOD with point sampling - // (they store values packed in two components), however, the fetch - // constant has anisotropic filtering enabled. However, Direct3D 12 - // doesn't allow mixing anisotropic and point filtering. Possibly - // anistropic filtering should be disabled when explicit LOD is used - do - // this here. + // 4D5307F2 uses vertex displacement map textures for tessellated models + // like the beehive tree with explicit LOD with point sampling (they store + // values packed in two components), however, the fetch constant has + // anisotropic filtering enabled. However, Direct3D 12 doesn't allow + // mixing anisotropic and point filtering. Possibly anistropic filtering + // should be disabled when explicit LOD is used - do this here. uint32_t sampler_binding_index = FindOrAddSamplerBinding( tfetch_index, instr.attributes.mag_filter, instr.attributes.min_filter, instr.attributes.mip_filter, diff --git a/src/xenia/gpu/dxbc_shader_translator_om.cc b/src/xenia/gpu/dxbc_shader_translator_om.cc index 1002bf14d..a49500ec2 100644 --- a/src/xenia/gpu/dxbc_shader_translator_om.cc +++ b/src/xenia/gpu/dxbc_shader_translator_om.cc @@ -287,8 +287,7 @@ void DxbcShaderTranslator::StartPixelShader_LoadROVParameters() { dxbc::Src::R(system_temp_rov_params_, dxbc::Src::kYYYY)); // Choose in which 40-sample half of the tile the pixel is, for swapping // 40-sample columns when accessing the depth buffer - games expect this - // behavior when writing depth back to the EDRAM via color writing (GTA IV, - // Halo 3). + // behavior when writing depth back to the EDRAM via color writing (4D5307E6). // system_temp_rov_params_.x = tile-local sample 0 X >= 40 // system_temp_rov_params_.y = row offset // system_temp_rov_params_.z = X sample 0 position within the tile @@ -3282,7 +3281,7 @@ void DxbcShaderTranslator::ROV_DepthTo24Bit(uint32_t d24_temp, dxbc::Src::LF(float(0xFFFFFF))); // Round to the nearest even integer. This seems to be the correct way: // rounding towards zero gives 0xFF instead of 0x100 in clear shaders in, - // for instance, Halo 3, but other clear shaders in it are also broken if + // for instance, 4D5307E6, but other clear shaders in it are also broken if // 0.5 is added before ftou instead of round_ne. a_.OpRoundNE(d24_dest, d24_src); // Convert to fixed-point. diff --git a/src/xenia/gpu/gpu_flags.cc b/src/xenia/gpu/gpu_flags.cc index 5f73fd3c2..1510eeec0 100644 --- a/src/xenia/gpu/gpu_flags.cc +++ b/src/xenia/gpu/gpu_flags.cc @@ -28,16 +28,18 @@ DEFINE_bool( "the real reason why they're invalid is found.", "GPU"); +// Extremely bright screen borders in 4D5307E6. +// Reading between texels with half-pixel offset in 58410954. DEFINE_bool( half_pixel_offset, true, "Enable support of vertex half-pixel offset (D3D9 PA_SU_VTX_CNTL " "PIX_CENTER). Generally games are aware of the half-pixel offset, and " "having this enabled is the correct behavior (disabling this may " - "significantly break post-processing in some games, like Halo 3), but in " - "some games it might have been ignored, resulting in slight blurriness of " - "UI textures, for instance, when they are read between texels rather than " - "at texel centers (Banjo-Kazooie), or the leftmost/topmost pixels may not " - "be fully covered when MSAA is used with fullscreen passes.", + "significantly break post-processing in some games), but in certain games " + "it might have been ignored, resulting in slight blurriness of UI " + "textures, for instance, when they are read between texels rather than " + "at texel centers, or the leftmost/topmost pixels may not be fully covered " + "when MSAA is used with fullscreen passes.", "GPU"); DEFINE_int32(query_occlusion_fake_sample_count, 1000, diff --git a/src/xenia/gpu/graphics_system.cc b/src/xenia/gpu/graphics_system.cc index 5a1eb44ea..ec0fbaeb7 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,23 +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_->CreateHostContext(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()->CreateEmulationContext(); - }); + // Setup the context the command processor will do all its drawing in. + bool contexts_initialized = true; + processor_context = provider()->CreateEmulationContext(); + 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_->CreateHostContext(target_window_)); + })) { + contexts_initialized = false; + } + } } else { - processor_context = provider()->CreateEmulationContext(); + 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 5de398e94..15d62253d 100644 --- a/src/xenia/gpu/premake5.lua +++ b/src/xenia/gpu/premake5.lua @@ -36,7 +36,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/primitive_processor.cc b/src/xenia/gpu/primitive_processor.cc index b00e4ce50..12ba9cbe5 100644 --- a/src/xenia/gpu/primitive_processor.cc +++ b/src/xenia/gpu/primitive_processor.cc @@ -57,7 +57,7 @@ DEFINE_bool( // TODO(Triang3l): More investigation of the cache threshold as cache lookups // and insertions require global critical region locking, and insertions also // require protecting pages. At 1024, the cache only made the performance worse -// (Tony Hawk's American Wasteland, 16-bit primitive reset index replacement). +// (415607D4, 16-bit primitive reset index replacement). DEFINE_int32( primitive_processor_cache_min_indices, 4096, "Smallest number of guest indices to store in the cache to try reusing " @@ -247,14 +247,14 @@ bool PrimitiveProcessor::Process(ProcessingResult& result_out) { // games using tessellated strips / fans so far. switch (tessellation_mode) { case xenos::TessellationMode::kDiscrete: - // - Call of Duty 3 - nets above barrels in the beginning of the - // first mission (turn right after the end of the intro) - + // - 415607E1 - nets above barrels in the beginning of the first + // mission (turn right after the end of the intro) - // kTriangleList. host_vertex_shader_type = Shader::HostVertexShaderType::kTriangleDomainCPIndexed; break; case xenos::TessellationMode::kContinuous: - // - Viva Pinata - tree building with a beehive in the beginning + // - 4D5307F2 - tree building with a beehive in the beginning // (visible on the start screen behind the logo), waterfall in the // beginning - kTriangleList. host_vertex_shader_type = @@ -276,7 +276,7 @@ bool PrimitiveProcessor::Process(ProcessingResult& result_out) { Shader::HostVertexShaderType::kQuadDomainCPIndexed; break; case xenos::TessellationMode::kContinuous: - // - Defender - retro screen and beams in the main menu - kQuadList. + // - 58410823 - retro screen and beams in the main menu - kQuadList. host_vertex_shader_type = Shader::HostVertexShaderType::kQuadDomainCPIndexed; break; @@ -285,14 +285,14 @@ bool PrimitiveProcessor::Process(ProcessingResult& result_out) { } break; case xenos::PrimitiveType::kTrianglePatch: - // - Banjo-Kazooie: Nuts & Bolts - water - adaptive. - // - Halo 3 - water - adaptive. + // - 4D5307E6 - water - adaptive. + // - 4D5307ED - water - adaptive. host_vertex_shader_type = Shader::HostVertexShaderType::kTriangleDomainPatchIndexed; break; case xenos::PrimitiveType::kQuadPatch: - // - Fable II - continuous. - // - Viva Pinata - garden ground - adaptive. + // - 4D5307F1 - continuous. + // - 4D5307F2 - garden ground - adaptive. host_vertex_shader_type = Shader::HostVertexShaderType::kQuadDomainPatchIndexed; break; diff --git a/src/xenia/gpu/registers.h b/src/xenia/gpu/registers.h index 2fc23b2c8..69d922d8b 100644 --- a/src/xenia/gpu/registers.h +++ b/src/xenia/gpu/registers.h @@ -335,10 +335,10 @@ union alignas(uint32_t) PA_SU_SC_MODE_CNTL { uint32_t cull_back : 1; // +1 // 0 - front is CCW, 1 - front is CW. uint32_t face : 1; // +2 - // The game Fuse uses poly_mode 2 for triangles, which is "reserved" on R6xx - // and not defined on Adreno 2xx, but polymode_front/back_ptype are 0 - // (points) in this case in Fuse, which should not be respected for - // non-kDualMode as the game wants to draw filled triangles. + // 4541096E uses poly_mode 2 for triangles, which is "reserved" on R6xx and + // not defined on Adreno 2xx, but polymode_front/back_ptype are 0 (points) + // in this case in 4541096E, which should not be respected for non-kDualMode + // as the title wants to draw filled triangles. xenos::PolygonModeEnable poly_mode : 2; // +3 xenos::PolygonType polymode_front_ptype : 3; // +5 xenos::PolygonType polymode_back_ptype : 3; // +8 @@ -559,16 +559,16 @@ union alignas(uint32_t) RB_COLORCONTROL { // (gl_FragCoord.y near 0 in the top, near 1 in the bottom here - D3D-like.) // For 2 samples, the top sample (closer to gl_FragCoord.y 0) is covered // when alpha is in [0.5, 1), the bottom sample is covered when the alpha is - // [1. With these thresholds, however, in Red Dead Redemption, almost all - // distant trees are transparent, this is asymmetric - fully transparent for - // a quarter of the range (or even half of the range for 2x and almost the - // entire range for 1x), but fully opaque only in one value. + // [1. With these thresholds, however, in 5454082B, almost all distant trees + // are transparent, this is asymmetric - fully transparent for a quarter of + // the range (or even half of the range for 2x and almost the entire range + // for 1x), but fully opaque only in one value. // Though, 2, 2, 2, 2 offset values are commonly used for undithered alpha - // to coverage (in games such as Red Dead Redemption, and overall in AMD - // driver implementations) - it appears that 2, 2, 2, 2 offsets are supposed - // to make this symmetric. - // Both Red Dead Redemption and RADV (which used AMDVLK as a reference) use - // 3, 1, 0, 2 offsets for dithered alpha to mask. + // to coverage (in games such as 5454082B, and overall in AMD driver + // implementations) - it appears that 2, 2, 2, 2 offsets are supposed to + // make this symmetric. + // Both 5454082B and RADV (which used AMDVLK as a reference) use 3, 1, 0, 2 + // offsets for dithered alpha to mask. // https://gitlab.freedesktop.org/nchery/mesa/commit/8a52e4cc4fad4f1c75acc0badd624778f9dfe202 // It appears that the offsets lower the thresholds by (offset / 4 / // sample count). That's consistent with both 2, 2, 2, 2 making the test diff --git a/src/xenia/gpu/render_target_cache.cc b/src/xenia/gpu/render_target_cache.cc index 76a3a2ef8..2dcf93789 100644 --- a/src/xenia/gpu/render_target_cache.cc +++ b/src/xenia/gpu/render_target_cache.cc @@ -40,6 +40,7 @@ DEFINE_bool( "reduce bandwidth usage during transfers as the previous depth won't need " "to be read.", "GPU"); +// The round trip is done, in particular, in 545407F2. DEFINE_string( depth_float24_conversion, "", "Method for converting 32-bit Z values to 20e4 floating point when using " @@ -56,8 +57,8 @@ DEFINE_string( " + Highest performance, allows early depth test and writing.\n" " + Host MSAA is possible with pixel-rate shading where supported.\n" " - EDRAM > RAM > EDRAM depth buffer round trip done in certain games " - "(such as GTA IV) destroys precision irreparably, causing artifacts if " - "another rendering pass is done after the EDRAM reupload.\n" + "destroys precision irreparably, causing artifacts if another rendering " + "pass is done after the EDRAM reupload.\n" " truncate:\n" " Convert to 20e4 directly in pixel shaders, always rounding down.\n" " + Average performance, conservative early depth test is possible.\n" @@ -96,18 +97,15 @@ DEFINE_bool( "bloom, etc., in some cases.", "GPU"); // Disabled by default because of full-screen effects that occur when game -// shaders assume piecewise linear, much more severe than blending-related -// issues. +// shaders assume piecewise linear (4541080F), much more severe than +// blending-related issues. DEFINE_bool( gamma_render_target_as_srgb, false, "When the host can't write piecewise linear gamma directly with correct " "blending, use sRGB output on the host for conceptually correct blending " - "in linear color space (to prevent issues such as bright squares around " - "bullet holes and overly dark lighting in Halo 3) while having slightly " - "different precision distribution in the render target and severely " - "incorrect values if the game accesses the resulting colors directly as " - "raw data (the whole screen in The Orange Box, for instance, since when " - "the first loading bar appears).", + "in linear color space while having slightly different precision " + "distribution in the render target and severely incorrect values if the " + "game accesses the resulting colors directly as raw data.", "GPU"); DEFINE_bool( mrt_edram_used_range_clamp_to_min, true, @@ -493,9 +491,9 @@ bool RenderTargetCache::Update(bool is_rasterization_done, // (issues caused by color and depth render target collisions haven't been // found yet), but render targets with smaller index are considered more // important - specifically, because of the usage in the lighting pass of - // Halo 3, which can be checked in the vertical look calibration sequence in + // 4D5307E6, which can be checked in the vertical look calibration sequence in // the beginning of the game: if render target 0 is removed in favor of 1, the - // UNSC servicemen and the world will be too dark, like fully in shadow - + // characters and the world will be too dark, like fully in shadow - // especially prominent on the helmet. This happens because the shader picks // between two render targets to write dynamically (though with a static, bool // constant condition), but all other state is set up in a way that implies @@ -624,7 +622,7 @@ bool RenderTargetCache::Update(bool is_rasterization_done, // "As if it was 64bpp" (contribution of 32bpp render targets multiplied by 2, // and clamping for 32bpp render targets divides this by 2) because 32bpp // render targets can be combined with twice as long 64bpp render targets. An - // example is the Dead Space 3 menu background (1-sample 1152x720, or 1200x720 + // example is the 4541099D menu background (1-sample 1152x720, or 1200x720 // after rounding to tiles, with a 32bpp depth buffer at 0 requiring 675 // tiles, and a 64bpp color buffer at 675 requiring 1350 tiles, but the // smallest distance between two render target bases is 675 tiles). diff --git a/src/xenia/gpu/render_target_cache.h b/src/xenia/gpu/render_target_cache.h index a3d580356..bf7c9a83e 100644 --- a/src/xenia/gpu/render_target_cache.h +++ b/src/xenia/gpu/render_target_cache.h @@ -70,10 +70,10 @@ class RenderTargetCache { // Significant differences: // - 8_8_8_8_GAMMA - the piecewise linear gamma curve is very different than // sRGB, one possible path is conversion in shaders (resulting in - // incorrect blending, especially visible on decals in Halo 3), another is - // using sRGB render targets and either conversion on resolve or reading - // the resolved data as a true sRGB texture (incorrect when the game - // accesses the data directly, like The Orange Box). + // incorrect blending, especially visible on decals in 4D5307E6), another + // is using sRGB render targets and either conversion on resolve or + // reading the resolved data as a true sRGB texture (incorrect when the + // game accesses the data directly, like 4541080F). // - 2_10_10_10_FLOAT - ranges significantly different than in float16, much // smaller RGB range, and alpha is fixed-point and has only 2 bits. // - 16_16, 16_16_16_16 - has -32 to 32 range, not -1 to 1 - need either to @@ -445,9 +445,9 @@ class RenderTargetCache { // aliasing naively, precision may be lost - host depth must only be // overwritten if the new guest value is different than the current host depth // when converted to the guest format (this catches the usual case of - // overwriting the depth buffer for clearing it mostly). Sonic the Hedgehog's - // intro cutscene, for example, has a good example of corruption that happens - // if this is not handled - the upper 1280x384 pixels are rendered in a very + // overwriting the depth buffer for clearing it mostly). 534507D6 intro + // cutscene, for example, has a good example of corruption that happens if + // this is not handled - the upper 1280x384 pixels are rendered in a very // "striped" way if the depth precision is lost (if this is made always return // false). virtual bool IsHostDepthEncodingDifferent( @@ -627,7 +627,7 @@ class RenderTargetCache { // surface info was changed), to avoid unneeded render target switching (which // is especially undesirable on tile-based GPUs) in the implementation if // simply disabling depth / stencil test or color writes and then re-enabling - // (Banjo-Kazooie does this often with color). Must also be used to determine + // (58410954 does this often with color). Must also be used to determine // whether it's safe to enable depth / stencil or writing to a specific color // render target in the pipeline for this draw call. // Only valid for non-pixel-shader-interlock paths. diff --git a/src/xenia/gpu/shader.h b/src/xenia/gpu/shader.h index d9af0feba..251f73d4e 100644 --- a/src/xenia/gpu/shader.h +++ b/src/xenia/gpu/shader.h @@ -551,7 +551,7 @@ struct ParsedAluInstruction { InstructionResult scalar_result; // Both operations must be executed before any result is stored if vector and // scalar operations are paired. There are cases of vector result being used - // as scalar operand or vice versa (the halo on Avalanche in Halo 3, for + // as scalar operand or vice versa (the ring on Avalanche in 4D5307E6, for // example), in this case there must be no dependency between the two // operations. @@ -854,11 +854,11 @@ class Shader { // highest static register address + 1, or 0 if no registers referenced this // way. SQ_PROGRAM_CNTL is not always reliable - some draws (like single point // draws with oPos = 0001 that are done by Xbox 360's Direct3D 9 sometimes; - // can be reproduced by launching Arrival in Halo 3 from the campaign lobby) - // that aren't supposed to cover any pixels use an invalid (zero) - // SQ_PROGRAM_CNTL, but with an outdated pixel shader loaded, in this case - // SQ_PROGRAM_CNTL may contain a number smaller than actually needed by the - // pixel shader - SQ_PROGRAM_CNTL should be used to go above this count if + // can be reproduced by launching the intro mission in 4D5307E6 from the + // campaign lobby) that aren't supposed to cover any pixels use an invalid + // (zero) SQ_PROGRAM_CNTL, but with an outdated pixel shader loaded, in this + // case SQ_PROGRAM_CNTL may contain a number smaller than actually needed by + // the pixel shader - SQ_PROGRAM_CNTL should be used to go above this count if // uses_register_dynamic_addressing is true. uint32_t register_static_address_bound() const { return register_static_address_bound_; diff --git a/src/xenia/gpu/shader_compiler_main.cc b/src/xenia/gpu/shader_compiler_main.cc index ebc445b38..ab6b805fd 100644 --- a/src/xenia/gpu/shader_compiler_main.cc +++ b/src/xenia/gpu/shader_compiler_main.cc @@ -16,9 +16,9 @@ #include "third_party/glslang/SPIRV/disassemble.h" #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" @@ -250,5 +250,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/shader_translator.cc b/src/xenia/gpu/shader_translator.cc index 8f6c61399..b1d1a060e 100644 --- a/src/xenia/gpu/shader_translator.cc +++ b/src/xenia/gpu/shader_translator.cc @@ -388,8 +388,8 @@ void Shader::GatherAluInstructionInformation( // allocation in shader translator implementations. // eA is (hopefully) always written to using: // mad eA, r#, const0100, c# - // (though there are some exceptions, shaders in Halo 3 for some reason set eA - // to zeros, but the swizzle of the constant is not .xyzw in this case, and + // (though there are some exceptions, shaders in 4D5307E6 for some reason set + // eA to zeros, but the swizzle of the constant is not .xyzw in this case, and // they don't write to eM#). // Export is done to vector_dest of the ucode instruction for both vector and // scalar operations - no need to check separately. diff --git a/src/xenia/gpu/shaders/adaptive_triangle.hs.hlsl b/src/xenia/gpu/shaders/adaptive_triangle.hs.hlsl index 31f7c36dd..c88563d79 100644 --- a/src/xenia/gpu/shaders/adaptive_triangle.hs.hlsl +++ b/src/xenia/gpu/shaders/adaptive_triangle.hs.hlsl @@ -36,7 +36,7 @@ XeHSConstantDataOutput XePatchConstant( // 2) r0.zyx -> r0.zyx by the guest (because r1.y is set to 0 by Xenia, which // apparently means identity swizzle to games). // 3) r0.z * v0 + r0.y * v1 + r0.x * v2 by the guest. - // With this order, there are no cracks in Halo 3 water. + // With this order, there are no cracks in 4D5307E6 water. [unroll] for (i = 0u; i < 3u; ++i) { output.edges[i] = xe_input_patch[(i + 1u) % 3u].edge_factor; } diff --git a/src/xenia/gpu/shaders/pixel_formats.hlsli b/src/xenia/gpu/shaders/pixel_formats.hlsli index 91bde0378..473a0620d 100644 --- a/src/xenia/gpu/shaders/pixel_formats.hlsli +++ b/src/xenia/gpu/shaders/pixel_formats.hlsli @@ -986,11 +986,11 @@ uint4 XeDXT3AAs1111TwoBlocksRowToBGRA4(uint2 halfblocks) { // DXT1/DXT3/DXT5 color components and CTX1 X/Y are ordered in: // http://fileadmin.cs.lth.se/cs/Personal/Michael_Doggett/talks/unc-xenos-doggett.pdf // (LSB on the right, MSB on the left.) - // TODO(Triang3l): Investigate this better, Halo: Reach is the only known game + // TODO(Triang3l): Investigate this better, 4D53085B is the only known game // that uses it (for lighting in certain places - one of easy to notice usages - // is the T-shaped (or somewhat H-shaped) metal beams in the beginning of - // Winter Contingency), however the contents don't say anything about the - // channel order. + // is the T-shaped (or somewhat H-shaped) metal beams in the beginning of the + // first mission), however the contents don't say anything about the channel + // order. uint4 row = (((halfblocks.xxyy >> uint2(3u, 11u).xyxy) & 1u) << 8u) | (((halfblocks.xxyy >> uint2(7u, 15u).xyxy) & 1u) << 24u) | (((halfblocks.xxyy >> uint2(2u, 10u).xyxy) & 1u) << 4u) | diff --git a/src/xenia/gpu/shaders/tessellation_adaptive.vs.hlsl b/src/xenia/gpu/shaders/tessellation_adaptive.vs.hlsl index fff6c501c..87ed1320a 100644 --- a/src/xenia/gpu/shaders/tessellation_adaptive.vs.hlsl +++ b/src/xenia/gpu/shaders/tessellation_adaptive.vs.hlsl @@ -5,8 +5,8 @@ XeHSControlPointInputAdaptive main(uint xe_edge_factor : SV_VertexID) { XeHSControlPointInputAdaptive output; // The Xbox 360's GPU accepts the float32 tessellation factors for edges // through a special kind of an index buffer. - // While Viva Pinata sets the factors to 0 for frustum-culled (quad) patches, - // in Halo 3 only allowing patches with factors above 0 makes distant + // While 4D5307F2 sets the factors to 0 for frustum-culled (quad) patches, in + // 4D5307E6 only allowing patches with factors above 0 makes distant // (triangle) patches disappear - it appears that there are no special values // for culled patches on the Xbox 360 (unlike zero, negative and NaN on // Direct3D 11). diff --git a/src/xenia/gpu/shaders/texture_load_ctx1.cs.hlsl b/src/xenia/gpu/shaders/texture_load_ctx1.cs.hlsl index eb908cc3a..6523cc92d 100644 --- a/src/xenia/gpu/shaders/texture_load_ctx1.cs.hlsl +++ b/src/xenia/gpu/shaders/texture_load_ctx1.cs.hlsl @@ -11,7 +11,7 @@ RWBuffer xe_texture_load_dest : register(u0); // Dword 1: // rrrrrrrrgggggggg // RRRRRRRRGGGGGGGG -// (R is in the higher bits, according to how this format is used in Halo 3). +// (R is in the higher bits, according to how this format is used in 4D5307E6). // Dword 2: // AA BB CC DD // EE FF GG HH diff --git a/src/xenia/gpu/shared_memory.cc b/src/xenia/gpu/shared_memory.cc index 0e47bd912..2b05821dc 100644 --- a/src/xenia/gpu/shared_memory.cc +++ b/src/xenia/gpu/shared_memory.cc @@ -465,9 +465,10 @@ std::pair SharedMemory::MemoryInvalidationCallback( // invalidated - if no GPU-written data nearby that was not intended to be // invalidated since it's not in sync with CPU memory and can't be // reuploaded. It's a lot cheaper to upload some excess data than to catch - // access violations - with 4 KB callbacks, the original Doom runs at 4 FPS - // on Intel Core i7-3770, with 64 KB the CPU game code takes 3 ms to run per - // frame, but with 256 KB it's 0.7 ms. + // access violations - with 4 KB callbacks, 58410824 (being a + // software-rendered game) runs at 4 FPS on Intel Core i7-3770, with 64 KB, + // the CPU game code takes 3 ms to run per frame, but with 256 KB, it's + // 0.7 ms. if (page_first & 63) { uint64_t gpu_written_start = system_page_flags_[block_first].valid_and_gpu_written; diff --git a/src/xenia/gpu/texture_conversion.cc b/src/xenia/gpu/texture_conversion.cc index bd028f47e..225e7feed 100644 --- a/src/xenia/gpu/texture_conversion.cc +++ b/src/xenia/gpu/texture_conversion.cc @@ -49,7 +49,8 @@ void CopySwapBlock(xenos::Endian endian, void* output, const void* input, void ConvertTexelCTX1ToR8G8(xenos::Endian endian, void* output, const void* input, size_t length) { // https://fileadmin.cs.lth.se/cs/Personal/Michael_Doggett/talks/unc-xenos-doggett.pdf - // (R is in the higher bits, according to how this format is used in Halo 3). + // (R is in the higher bits, according to how this format is used in + // 4D5307E6). union { uint8_t data[8]; struct { diff --git a/src/xenia/gpu/texture_util.cc b/src/xenia/gpu/texture_util.cc index bd7a78c6c..e64e4c205 100644 --- a/src/xenia/gpu/texture_util.cc +++ b/src/xenia/gpu/texture_util.cc @@ -352,11 +352,11 @@ TextureGuestLayout GetGuestTextureLayout( xenos::kTextureSubresourceAlignmentBytes); // Estimate the memory amount actually referenced by the texture, which may - // be smaller (especially in the 1280x720 linear k_8_8_8_8 case in Ridge - // Racer Unbounded, for which memory exactly for 1280x720 is allocated, and - // aligning the height to 32 would cause access of an unallocated page) or - // bigger than the stride. For tiled textures, this is the dimensions - // aligned to 32x32x4 blocks (or x1 for the missing dimensions). + // be smaller (especially in the 1280x720 linear k_8_8_8_8 case in 4E4D083E, + // for which memory exactly for 1280x720 is allocated, and aligning the + // height to 32 would cause access of an unallocated page) or bigger than + // the stride. For tiled textures, this is the dimensions aligned to 32x32x4 + // blocks (or x1 for the missing dimensions). uint32_t level_width_blocks = xe::align(std::max(width_texels >> level, uint32_t(1)), format_info->block_width) / diff --git a/src/xenia/gpu/texture_util.h b/src/xenia/gpu/texture_util.h index 3d9bc0e99..733b3e88f 100644 --- a/src/xenia/gpu/texture_util.h +++ b/src/xenia/gpu/texture_util.h @@ -64,14 +64,14 @@ bool GetPackedMipOffset(uint32_t width, uint32_t height, uint32_t depth, // implies 32-block alignment for both uncompressed and compressed textures) // stored in the fetch constant, and height aligned to 32 blocks for Z slice // and array layer stride calculation purposes. The pitch can be different -// from the actual width - an example is Plants vs. Zombies, using 1408 pitch -// for a 1280x menu background). +// from the actual width - an example is 584109FF, using 1408 pitch for a +// 1280x menu background). // - The mip levels use `max(next_pow2(width or height in texels) >> level, 1)` // aligned to 32 blocks for the same purpose, likely disregarding the pitch // from the fetch constant. // // There is also mip tail packing if the fetch constant specifies that packed -// mips are enabled, for both tiled and linear textures (Prey uses linear +// mips are enabled, for both tiled and linear textures (545407E0 uses linear // DXT-compressed textures with packed mips very extensively for the game world // materials). In this case, mips with width or height of 16 or smaller are // stored not individually, but instead, in 32-texel (note: not 32-block - mip @@ -99,7 +99,7 @@ bool GetPackedMipOffset(uint32_t width, uint32_t height, uint32_t depth, // tail, and the offset calculation function doesn't have level == 0 checks in // it, only early-out if level < packed tail level (which can be 0). There are // examples of textures with packed base, for example, in the intro level of -// Prey (8x8 linear DXT1 - pairs of orange lights in the bottom of gambling +// 545407E0 (8x8 linear DXT1 - pairs of orange lights in the bottom of gambling // machines). // // Linear texture rows are aligned to 256 bytes, for both the base and the mips @@ -107,22 +107,21 @@ bool GetPackedMipOffset(uint32_t width, uint32_t height, uint32_t depth, // fetch constant). // // However, all the 32x32x4 padding, being just padding, is not necessarily -// being actually accessed, especially for linear textures. Ridge Racer -// Unbounded has a 1280x720 k_8_8_8_8 linear texture, and allocates memory for -// exactly 1280x720, so aligning the height to 32 to 1280x736 results in access -// violations. So, while for stride calculations all the padding must be -// respected, for actual memory loads it's better to avoid trying to access it -// when possible: +// being actually accessed, especially for linear textures. 4E4D083E has a +// 1280x720 k_8_8_8_8 linear texture, and allocates memory for exactly 1280x720, +// so aligning the height to 32 to 1280x736 results in access violations. So, +// while for stride calculations all the padding must be respected, for actual +// memory loads it's better to avoid trying to access it when possible: // - If the pitch is bigger than the width, it's better to calculate the last // row's length from the width rather than the pitch (this also possibly works // in the other direction though - pitch < width is a weird situation, but // probably legal, and may lead to reading data from beyond the calculated // subresource stride). -// - For linear textures (like that 1280x720 example from Ridge Racer -// Unbounded), it's easy to calculate the exact memory extent that may be -// accessed knowing the dimensions (unlike for tiled textures with complex -// addressing within 32x32x4-block tiles), so there's no need to align them to -// 32x32x4 for memory extent calculation. +// - For linear textures (like that 1280x720 example from 4E4D083E), it's easy +// to calculate the exact memory extent that may be accessed knowing the +// dimensions (unlike for tiled textures with complex addressing within +// 32x32x4-block tiles), so there's no need to align them to 32x32x4 for +// memory extent calculation. // - For the linear packed mip tail, the extent can be calculated as max of // (block offsets + block extents) of all levels stored in it. // @@ -152,16 +151,16 @@ struct TextureGuestLayout { // tiled textures, this will be rounded to 32x32x4 blocks (or 32x32x1 // depending on the dimension), but for the linear subresources, this may be // significantly (including less 4 KB pages) smaller than the aligned size - // (like for Ridge Racer Unbounded where aligning the height of a 1280x720 - // linear texture results in access violations). For the linear mip tail, - // this includes all the mip levels stored in it. If the width is bigger - // than the pitch, this will also be taken into account for the last row so - // all memory actually used by the texture will be loaded, and may be bigger - // than the distance between array slices or levels. The purpose of this - // parameter is to make the memory amount that needs to be resident as close - // to the real amount as possible, to make sure all the needed data will be - // read, but also, if possible, unneeded memory pages won't be accessed - // (since that may trigger an access violation on the CPU). + // (like for 4E4D083E where aligning the height of a 1280x720 linear texture + // results in access violations). For the linear mip tail, this includes all + // the mip levels stored in it. If the width is bigger than the pitch, this + // will also be taken into account for the last row so all memory actually + // used by the texture will be loaded, and may be bigger than the distance + // between array slices or levels. The purpose of this parameter is to make + // the memory amount that needs to be resident as close to the real amount + // as possible, to make sure all the needed data will be read, but also, if + // possible, unneeded memory pages won't be accessed (since that may trigger + // an access violation on the CPU). uint32_t x_extent_blocks; uint32_t y_extent_blocks; uint32_t z_extent; 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/ucode.h b/src/xenia/gpu/ucode.h index a4a169eb0..9d5ed7dca 100644 --- a/src/xenia/gpu/ucode.h +++ b/src/xenia/gpu/ucode.h @@ -483,7 +483,7 @@ enum class FetchOpcode : uint32_t { // - 3D (used for both 3D and stacked 2D texture): U, V, W (normalized or // unnormalized - same for both 3D W and stack layer; also VolMagFilter / // VolMinFilter between stack layers is supported, used for color correction - // in Burnout Revenge). + // in 454107DC). // - Cube: SC, TC (between 1 and 2 for normalized), face ID (0.0 to 5.0), the // cube vector ALU instruction is used to calculate them. // https://gpuopen.com/learn/fetching-from-cubes-and-octahedrons/ @@ -495,9 +495,9 @@ enum class FetchOpcode : uint32_t { // The total LOD for a sample is additive and is based on what is enabled. // // For cube maps, according to what texCUBEgrad compiles to in a modified - // HLSL shader of Brave: A Warrior's Tale and to XNA assembler output for PC - // SM3 texldd, register gradients are in cube space (not in SC/TC space, - // unlike the coordinates themselves). This isn't true for the GCN, however. + // HLSL shader of 455607D1 and to XNA assembler output for PC SM3 texldd, + // register gradients are in cube space (not in SC/TC space, unlike the + // coordinates themselves). This isn't true for the GCN, however. // // TODO(Triang3l): Find if gradients are unnormalized for cube maps if // coordinates are unnormalized. Since texldd doesn't perform any @@ -814,8 +814,8 @@ static_assert_size(TextureFetchInstruction, sizeof(uint32_t) * 3); // (mul, mad, dp, etc.) and for NaN in min/max. It's very important to respect // this rule for multiplication, as games often rely on it in vector // normalization (rcp and mul), Infinity * 0 resulting in NaN breaks a lot of -// things in games - causes white screen in Halo 3, white specular on -// characters in GTA IV. The result is always positive zero in this case, no +// things in games - causes white screen in 4D5307E6, white specular on +// characters in 545407F2. The result is always positive zero in this case, no // matter what the signs of the other operands are, according to R5xx // Acceleration section 8.7.5 "Legacy multiply behavior" and testing on // Adreno 200. This means that the following need to be taken into account @@ -1628,8 +1628,8 @@ enum class ExportRegister : uint32_t { // X - PSIZE (gl_PointSize). // Y - EDGEFLAG (glEdgeFlag) for PrimitiveType::kPolygon wireframe/point // drawing. - // Z - KILLVERTEX flag (used in Banjo-Kazooie: Nuts & Bolts for grass), set - // for killing primitives based on PA_CL_CLIP_CNTL::VTX_KILL_OR condition. + // Z - KILLVERTEX flag (used in 4D5307ED for grass), set for killing + // primitives based on PA_CL_CLIP_CNTL::VTX_KILL_OR condition. kVSPointSizeEdgeFlagKillVertex = 63, kPSColor0 = 0, diff --git a/src/xenia/gpu/vulkan/vulkan_graphics_system.cc b/src/xenia/gpu/vulkan/vulkan_graphics_system.cc index 36e0f3899..9f89f1e62 100644 --- a/src/xenia/gpu/vulkan/vulkan_graphics_system.cc +++ b/src/xenia/gpu/vulkan/vulkan_graphics_system.cc @@ -24,7 +24,7 @@ VulkanGraphicsSystem::~VulkanGraphicsSystem() {} X_STATUS VulkanGraphicsSystem::Setup(cpu::Processor* processor, kernel::KernelState* kernel_state, ui::Window* target_window) { - 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/xenos.h b/src/xenia/gpu/xenos.h index d3a202dd6..2a0b6c938 100644 --- a/src/xenia/gpu/xenos.h +++ b/src/xenia/gpu/xenos.h @@ -235,10 +235,10 @@ enum class SurfaceNumFormat : uint32_t { // // Depth surfaces are also stored as 32bpp tiles, however, as opposed to color // surfaces, 40x16-sample halves of each tile are swapped - game shaders (for -// example, in GTA IV, Halo 3) perform this swapping when writing specific -// depth/stencil values by drawing to a depth buffer's memory through a color -// render target (to reupload a depth/stencil surface previously evicted from -// the EDRAM to the main memory, for instance). +// example, in 4D5307E6 main menu, 545407F2) perform this swapping when writing +// specific depth/stencil values by drawing to a depth buffer's memory through a +// color render target (to reupload a depth/stencil surface previously evicted +// from the EDRAM to the main memory, for instance). enum class MsaaSamples : uint32_t { k1X = 0, @@ -728,12 +728,12 @@ enum class SampleControl : uint32_t { // - sample_control is SQ_CONTEXT_MISC::sc_sample_cntl. // - interpolator_control_sampling_pattern is // SQ_INTERPOLATOR_CNTL::sampling_pattern. -// Centroid interpolation can be tested in Red Dead Redemption. If the GPU host -// backend implements guest MSAA properly, using host MSAA, with everything -// interpolated at centers, the Diez Coronas start screen background may have -// a few distinctly bright pixels on the mesas/buttes, where extrapolation -// happens. Interpolating certain values (ones that aren't used for gradient -// calculation, not texture coordinates) at centroids fixes this issue. +// Centroid interpolation can be tested in 5454082B. If the GPU host backend +// implements guest MSAA properly, using host MSAA, with everything interpolated +// at centers, the Monument Valley start screen background may have a few +// distinctly bright pixels on the mesas/buttes, where extrapolation happens. +// Interpolating certain values (ones that aren't used for gradient calculation, +// not texture coordinates) at centroids fixes this issue. inline uint32_t GetInterpolatorSamplingPattern( MsaaSamples msaa_samples, SampleControl sample_control, uint32_t interpolator_control_sampling_pattern) { @@ -763,9 +763,9 @@ enum class TessellationMode : uint32_t { enum class PolygonModeEnable : uint32_t { kDisabled = 0, // Render triangles. kDualMode = 1, // Send 2 sets of 3 polygons with the specified polygon type. - // The game Fuse uses 2 for triangles, which is "reserved" on R6xx and not - // defined on Adreno 2xx, but polymode_front/back_ptype are 0 (points) in this - // case in Fuse, which should not be respected for non-kDualMode as the game + // 4541096E uses 2 for triangles, which is "reserved" on R6xx and not defined + // on Adreno 2xx, but polymode_front/back_ptype are 0 (points) in this case in + // 4541096E, which should not be respected for non-kDualMode as the title // wants to draw filled triangles. }; @@ -785,17 +785,15 @@ enum class ModeControl : uint32_t { // for it especially since the Xbox 360 doesn't have early per-sample depth / // stencil, only early hi-Z / hi-stencil, and other registers possibly // toggling pixel shader execution are yet to be found): - // - Most of depth pre-pass draws in Call of Duty 4 use the kDepth more with - // a `oC0 = tfetch2D(tf0, r0.xy) * r1` shader, some use `oC0 = r0` though. + // - Most of depth pre-pass draws in 415607E6 use the kDepth more with a + // `oC0 = tfetch2D(tf0, r0.xy) * r1` shader, some use `oC0 = r0` though. // However, when alphatested surfaces are drawn, kColorDepth is explicitly // used with the same shader performing the texture fetch. - // - Red Dead Redemption has some kDepth draws with alphatest enabled, but the - // shader is `oC0 = r0`, which makes no sense (alphatest based on an - // interpolant from the vertex shader) as no texture alpha cutout is - // involved. - // - Red Dead Redemption also has kDepth draws with pretty complex shaders - // clearly for use only in the color pass - even fetching and filtering a - // shadowmap. + // - 5454082B has some kDepth draws with alphatest enabled, but the shader is + // `oC0 = r0`, which makes no sense (alphatest based on an interpolant from + // the vertex shader) as no texture alpha cutout is involved. + // - 5454082B also has kDepth draws with pretty complex shaders clearly for + // use only in the color pass - even fetching and filtering a shadowmap. // For now, based on these, let's assume the pixel shader is never used with // kDepth. kDepth = 5, @@ -833,10 +831,10 @@ enum class ModeControl : uint32_t { // coordinates of the corners). // // The rectangle is used for both the source render target and the destination -// texture, according to how it's used in Tales of Vesperia. +// texture, according to how it's used in 4E4D07E9. // // Direct3D 9 gives the rectangle in source render target coordinates (for -// example, in Halo 3, the sniper rifle scope has a (128,64)->(448,256) +// example, in 4D5307E6, the sniper rifle scope has a (128,64)->(448,256) // rectangle). It doesn't adjust the EDRAM base pointer, otherwise (taking into // account that 4x MSAA is used for the scope) it would have been // (8,0)->(328,192), but it's not. However, it adjusts the destination texture @@ -851,7 +849,7 @@ enum class ModeControl : uint32_t { // RB_COPY_DEST_PITCH's purpose appears to be not clamping or something like // that, but just specifying pitch for going between rows, and height for going // between 3D texture slices. copy_dest_pitch is rounded to 32 by Direct3D 9, -// copy_dest_height is not. In the Halo 3 sniper rifle scope example, +// copy_dest_height is not. In the 4D5307E6 sniper rifle scope example, // copy_dest_pitch is 320, and copy_dest_height is 192 - the same as the resolve // rectangle size (resolving from a 320x192 portion of the surface at 128,64 to // the whole texture, at 0,0). Relative to RB_COPY_DEST_BASE, the height should @@ -860,17 +858,17 @@ enum class ModeControl : uint32_t { // of the register) that it exists purely to be able to go between 3D texture // slices. // -// Window scissor must also be applied - in the jigsaw puzzle in Banjo-Tooie, -// there are 1280x720 resolve rectangles, but only the scissored 1280x256 -// needs to be copied, otherwise it overflows even beyond the EDRAM, and the -// depth buffer is visible on the screen. It also ensures the coordinates are -// not negative (in F.E.A.R., for example, the right tile is resolved with -// vertices (-640,0)->(640,720), however, the destination texture pointer is -// adjusted properly to the right half of the texture, and the source render -// target has a pitch of 800). +// Window scissor must also be applied - in the jigsaw puzzle in 58410955, there +// are 1280x720 resolve rectangles, but only the scissored 1280x256 needs to be +// copied, otherwise it overflows even beyond the EDRAM, and the depth buffer is +// visible on the screen. It also ensures the coordinates are not negative (in +// 565507D9, for example, the right tile is resolved with vertices +// (-640,0)->(640,720), however, the destination texture pointer is adjusted +// properly to the right half of the texture, and the source render target has a +// pitch of 800). // Granularity of offset and size in resolve operations is 8x8 pixels -// (GPU_RESOLVE_ALIGNMENT - for example, Halo 3 resolves a 24x16 region for a +// (GPU_RESOLVE_ALIGNMENT - for example, 4D5307E6 resolves a 24x16 region for a // 18x10 texture, 8x8 region for a 1x1 texture). // https://github.com/jmfauvel/CSGO-SDK/blob/master/game/client/view.cpp#L944 // https://github.com/stanriders/hl2-asw-port/blob/master/src/game/client/vgui_int.cpp#L901 @@ -1072,9 +1070,9 @@ union alignas(uint32_t) xe_gpu_texture_fetch_t { // pitch is irrelevant to them (but the 256-byte alignment requirement still // applies to linear textures). // Examples of pitch > aligned width: - // - Plants vs. Zombies (loading screen and menu backgrounds, 1408 for a - // 1280x linear k_DXT4_5 texture, which corresponds to 22 * 256 bytes - // rather than 20 * 256 for just 1280x). + // - 584109FF (loading screen and menu backgrounds, 1408 for a 1280x linear + // k_DXT4_5 texture, which corresponds to 22 * 256 bytes rather than + // 20 * 256 for just 1280x). uint32_t pitch : 9; // +22 uint32_t tiled : 1; // +31 diff --git a/src/xenia/hid/hid_demo.cc b/src/xenia/hid/hid_demo.cc index 018fdab80..6dff015da 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->CreateHostContext(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_->CreateHostContext(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 914f31bb8..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, @@ -344,7 +354,7 @@ X_RESULT SDLInputDriver::GetKeystroke(uint32_t users, uint32_t flags, if (!(butts_changed & fbutton)) { continue; } - ui::VirtualKey vk = kVkLookup.at(last.repeat_butt_idx); + ui::VirtualKey vk = kVkLookup.at(i); if (vk == ui::VirtualKey::kNone) { continue; } @@ -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/kernel_state.cc b/src/xenia/kernel/kernel_state.cc index c0d2e3d20..9c5dcebc2 100644 --- a/src/xenia/kernel/kernel_state.cc +++ b/src/xenia/kernel/kernel_state.cc @@ -316,6 +316,17 @@ void KernelState::SetExecutableModule(object_ref module) { *variable_ptr = executable_module_->hmodule_ptr(); } + // Setup the kernel's ExLoadedImageName field + export_entry = processor()->export_resolver()->GetExportByOrdinal( + "xboxkrnl.exe", ordinals::ExLoadedImageName); + + if (export_entry) { + char* variable_ptr = + memory()->TranslateVirtual(export_entry->variable_ptr); + xe::string_util::copy_truncating( + variable_ptr, executable_module_->path(), + xboxkrnl::XboxkrnlModule::kExLoadedImageNameSize); + } // Spin up deferred dispatch worker. // TODO(benvanik): move someplace more appropriate (out of ctor, but around // here). diff --git a/src/xenia/kernel/xam/apps/xgi_app.cc b/src/xenia/kernel/xam/apps/xgi_app.cc index 771341cdb..8fdf699f4 100644 --- a/src/xenia/kernel/xam/apps/xgi_app.cc +++ b/src/xenia/kernel/xam/apps/xgi_app.cc @@ -98,7 +98,7 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, return X_E_SUCCESS; } case 0x000B0014: { - // Gets Jetpac XBLA in game + // Gets 584107FB in game. // get high score table? XELOGD("XGI_unknown"); return X_STATUS_SUCCESS; diff --git a/src/xenia/kernel/xam/apps/xlivebase_app.cc b/src/xenia/kernel/xam/apps/xlivebase_app.cc index b8c72caf1..57bfc4fd4 100644 --- a/src/xenia/kernel/xam/apps/xlivebase_app.cc +++ b/src/xenia/kernel/xam/apps/xlivebase_app.cc @@ -47,7 +47,7 @@ X_HRESULT XLiveBaseApp::DispatchMessageSync(uint32_t message, // XONLINE_SERVICE_INFO structure. XELOGD("CXLiveLogon::GetServiceInfo({:08X}, {:08X})", buffer_ptr, buffer_length); - return 1229; // ERROR_CONNECTION_INVALID + return 0x80151802; // ERROR_CONNECTION_INVALID } case 0x00058020: { // 0x00058004 is called right before this. @@ -66,7 +66,7 @@ X_HRESULT XLiveBaseApp::DispatchMessageSync(uint32_t message, return X_E_FAIL; } case 0x00058046: { - // Required to be successful for Forza 4 to detect signed-in profile + // Required to be successful for 4D530910 to detect signed-in profile // Doesn't seem to set anything in the given buffer, probably only takes // input XELOGD("XLiveBaseUnk58046({:08X}, {:08X}) unimplemented", buffer_ptr, diff --git a/src/xenia/kernel/xam/content_manager.h b/src/xenia/kernel/xam/content_manager.h index 4876bfd1c..0db9f0cb5 100644 --- a/src/xenia/kernel/xam/content_manager.h +++ b/src/xenia/kernel/xam/content_manager.h @@ -73,8 +73,8 @@ struct XCONTENT_DATA { } void set_display_name(const std::u16string_view value) { - // Some games (eg Goldeneye XBLA) require multiple null-terminators for it - // to read the string properly, blanking the array should take care of that + // Some games (e.g. 584108A9) require multiple null-terminators for it to + // read the string properly, blanking the array should take care of that std::fill_n(display_name_raw.chars, countof(display_name_raw.chars), 0); string_util::copy_and_swap_truncating(display_name_raw.chars, value, diff --git a/src/xenia/kernel/xam/user_profile.cc b/src/xenia/kernel/xam/user_profile.cc index 4982a02c6..3bfa40f48 100644 --- a/src/xenia/kernel/xam/user_profile.cc +++ b/src/xenia/kernel/xam/user_profile.cc @@ -20,9 +20,8 @@ namespace kernel { namespace xam { UserProfile::UserProfile() { - // NeoGeo Battle Coliseum checks the user XUID against a mask of - // 0x00C0000000000000 (3<<54), if non-zero, it prevents the user from playing - // the game. + // 58410A1F checks the user XUID against a mask of 0x00C0000000000000 (3<<54), + // if non-zero, it prevents the user from playing the game. // "You do not have permissions to perform this operation." xuid_ = 0xB13EBABEBABEBABE; name_ = "User"; diff --git a/src/xenia/kernel/xam/xam_content.cc b/src/xenia/kernel/xam/xam_content.cc index 690edd6ca..dea81d46b 100644 --- a/src/xenia/kernel/xam/xam_content.cc +++ b/src/xenia/kernel/xam/xam_content.cc @@ -139,9 +139,9 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name, *disposition_ptr = 0; } - auto run = [content_manager, root_name, flags, content_data, disposition_ptr, - license_mask_ptr](uint32_t& extended_error, - uint32_t& length) -> X_RESULT { + auto run = [content_manager, root_name = root_name.value(), flags, + content_data, disposition_ptr, license_mask_ptr]( + uint32_t& extended_error, uint32_t& length) -> X_RESULT { X_RESULT result = X_ERROR_INVALID_PARAMETER; bool create = false; bool open = false; @@ -203,9 +203,9 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name, } if (create) { - result = content_manager->CreateContent(root_name.value(), content_data); + result = content_manager->CreateContent(root_name, content_data); } else if (open) { - result = content_manager->OpenContent(root_name.value(), content_data); + result = content_manager->OpenContent(root_name, content_data); } if (license_mask_ptr && XSUCCEEDED(result)) { diff --git a/src/xenia/kernel/xam/xam_enum.cc b/src/xenia/kernel/xam/xam_enum.cc index cd511c37a..396d08dd7 100644 --- a/src/xenia/kernel/xam/xam_enum.cc +++ b/src/xenia/kernel/xam/xam_enum.cc @@ -40,10 +40,9 @@ uint32_t xeXamEnumerate(uint32_t handle, uint32_t flags, lpvoid_t buffer_ptr, auto run = [e, buffer_ptr](uint32_t& extended_error, uint32_t& length) -> X_RESULT { X_RESULT result; - uint32_t item_count; + uint32_t item_count = 0; if (!buffer_ptr) { result = X_ERROR_INVALID_PARAMETER; - item_count = 0; } else { result = e->WriteItems(buffer_ptr.guest_address(), buffer_ptr.as(), &item_count); diff --git a/src/xenia/kernel/xam/xam_net.cc b/src/xenia/kernel/xam/xam_net.cc index dbdff9dc1..4c5c0ead4 100644 --- a/src/xenia/kernel/xam/xam_net.cc +++ b/src/xenia/kernel/xam/xam_net.cc @@ -249,8 +249,8 @@ dword_result_t NetDll_WSAStartup(dword_t caller, word_t version, data_ptr->max_sockets = wsaData.iMaxSockets; data_ptr->max_udpdg = wsaData.iMaxUdpDg; - // Some games (PoG) want this value round-tripped - they'll compare if it - // changes and bugcheck if it does. + // Some games (5841099F) want this value round-tripped - they'll compare if + // it changes and bugcheck if it does. uint32_t vendor_ptr = xe::load_and_swap(data_out + 0x190); xe::store_and_swap(data_out + 0x190, vendor_ptr); } @@ -459,7 +459,7 @@ dword_result_t NetDll_XNetGetTitleXnAddr(dword_t caller, // TODO(gibbed): A proper mac address. // RakNet's 360 version appears to depend on abEnet to create "random" 64-bit // numbers. A zero value will cause RakPeer::Startup to fail. This causes - // Peggle 2 to crash on startup. + // 58411436 to crash on startup. // The 360-specific code is scrubbed from the RakNet repo, but there's still // traces of what it's doing which match the game code. // https://github.com/facebookarchive/RakNet/blob/master/Source/RakPeer.cpp#L382 @@ -950,7 +950,7 @@ dword_result_t NetDll_recvfrom(dword_t caller, dword_t socket_handle, from_ptr->sin_family = native_from.sin_family; from_ptr->sin_port = native_from.sin_port; from_ptr->sin_addr = native_from.sin_addr; - memset(from_ptr->sin_zero, 0, 8); + std::memset(from_ptr->x_sin_zero, 0, sizeof(from_ptr->x_sin_zero)); } if (fromlen_ptr) { *fromlen_ptr = native_fromlen; diff --git a/src/xenia/kernel/xam/xam_notify.cc b/src/xenia/kernel/xam/xam_notify.cc index 32fb2635d..6d09df7f3 100644 --- a/src/xenia/kernel/xam/xam_notify.cc +++ b/src/xenia/kernel/xam/xam_notify.cc @@ -79,8 +79,8 @@ dword_result_t XNotifyGetNext(dword_t handle, dword_t match_id, } *id_ptr = dequeued ? id : 0; - // param_ptr may be null - Ghost Recon Advanced Warfighter 2 Demo explicitly - // passes nullptr in the code. + // param_ptr may be null - 555307F0 Demo explicitly passes nullptr in the + // code. // https://github.com/xenia-project/xenia/pull/1577 if (param_ptr) { *param_ptr = dequeued ? param : 0; 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_party.cc b/src/xenia/kernel/xam/xam_party.cc index fd0556baa..5476f0b33 100644 --- a/src/xenia/kernel/xam/xam_party.cc +++ b/src/xenia/kernel/xam/xam_party.cc @@ -17,8 +17,7 @@ namespace kernel { namespace xam { dword_result_t XamPartyGetUserList(dword_t player_count, lpdword_t party_list) { - // Sonic & All-Stars Racing Transformed want specificly this code - // to skip loading party data. + // 5345085D wants specifically this code to skip loading party data. // This code is not documented in NT_STATUS code list return 0x807D0003; } diff --git a/src/xenia/kernel/xam/xam_task.cc b/src/xenia/kernel/xam/xam_task.cc index facac462c..58f314079 100644 --- a/src/xenia/kernel/xam/xam_task.cc +++ b/src/xenia/kernel/xam/xam_task.cc @@ -11,6 +11,7 @@ #include "xenia/base/string_util.h" #include "xenia/cpu/processor.h" #include "xenia/kernel/kernel_state.h" +#include "xenia/kernel/user_module.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xam/xam_module.h" #include "xenia/kernel/xam/xam_private.h" @@ -40,24 +41,37 @@ static_assert_size(XTASK_MESSAGE, 0x1C); dword_result_t XamTaskSchedule(lpvoid_t callback, pointer_t message, - dword_t unknown, lpdword_t handle_ptr) { - assert_zero(unknown); - + lpdword_t unknown, lpdword_t handle_ptr) { // TODO(gibbed): figure out what this is for *handle_ptr = 12345; - XELOGW("!! Executing scheduled task ({:08X}) synchronously, PROBABLY BAD !! ", + uint32_t stack_size = kernel_state()->GetExecutableModule()->stack_size(); + + // Stack must be aligned to 16kb pages + stack_size = std::max((uint32_t)0x4000, ((stack_size + 0xFFF) & 0xFFFFF000)); + + auto thread = + object_ref(new XThread(kernel_state(), stack_size, 0, callback, + message.guest_address(), 0, true)); + + X_STATUS result = thread->Create(); + + if (XFAILED(result)) { + // Failed! + XELOGE("XAM task creation failed: {:08X}", result); + return result; + } + + XELOGD("XAM task ({:08X}) scheduled asynchronously", callback.guest_address()); - // TODO(gibbed): this is supposed to be async... let's cheat. - auto thread_state = XThread::GetCurrentThread()->thread_state(); - uint64_t args[] = {message.guest_address()}; - auto result = kernel_state()->processor()->Execute(thread_state, callback, - args, xe::countof(args)); return X_STATUS_SUCCESS; } DECLARE_XAM_EXPORT2(XamTaskSchedule, kNone, kImplemented, kSketchy); +dword_result_t XamTaskShouldExit(dword_t r3) { return 0; } +DECLARE_XAM_EXPORT2(XamTaskShouldExit, kNone, kStub, kSketchy); + void RegisterTaskExports(xe::cpu::ExportResolver* export_resolver, KernelState* kernel_state) {} 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/xam/xam_user.cc b/src/xenia/kernel/xam/xam_user.cc index aed0c765b..612cc326c 100644 --- a/src/xenia/kernel/xam/xam_user.cc +++ b/src/xenia/kernel/xam/xam_user.cc @@ -716,7 +716,7 @@ dword_result_t XamUserCreateAchievementEnumerator(dword_t title_id, i, // dummy image id 0, {0, 0}, - 8}; // flags=8 makes dummy achievements show up in Crackdown's + 8}; // flags=8 makes dummy achievements show up in 4D5307DC // achievements list. e->AppendItem(item); } diff --git a/src/xenia/kernel/xbdm/xbdm_misc.cc b/src/xenia/kernel/xbdm/xbdm_misc.cc index c352b2434..e15af9eb6 100644 --- a/src/xenia/kernel/xbdm/xbdm_misc.cc +++ b/src/xenia/kernel/xbdm/xbdm_misc.cc @@ -34,7 +34,7 @@ DECLARE_XBDM_EXPORT1(DmCloseLoadedModules, kDebug, kStub); MAKE_DUMMY_STUB_STATUS(DmFreePool); dword_result_t DmGetXbeInfo() { - // TODO(gibbed): Crackdown appears to expect this as success? + // TODO(gibbed): 4D5307DC appears to expect this as success? // Unknown arguments -- let's hope things don't explode. return 0x02DA0000; } diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_audio_xma.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_audio_xma.cc index 951732494..f4c2c156e 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_audio_xma.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_audio_xma.cc @@ -119,7 +119,7 @@ static_assert_size(XMA_CONTEXT_INIT, 56); dword_result_t XMAInitializeContext(lpvoid_t context_ptr, pointer_t context_init) { - // Input buffers may be null (buffer 1 in Tony Hawk's American Wasteland). + // Input buffers may be null (buffer 1 in 415607D4). // Convert to host endianness. uint32_t input_buffer_0_guest_ptr = context_init->input_buffer_0_ptr; uint32_t input_buffer_0_physical_address = 0; diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_debug.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_debug.cc index a3e92c55f..5dd7b3309 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_debug.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_debug.cc @@ -47,8 +47,8 @@ void HandleSetThreadName(pointer_t record) { return; } - // Shadowrun (and its demo) has a bug where it ends up passing freed memory - // for the name, so at the point of SetThreadName it's filled with junk. + // 4D5307D6 (and its demo) has a bug where it ends up passing freed memory for + // the name, so at the point of SetThreadName it's filled with junk. // TODO(gibbed): cvar for thread name encoding for conversion, some games use // SJIS and there's no way to automatically know this. diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc index 38d557eef..37dc42aec 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_io.cc @@ -49,10 +49,10 @@ static bool IsValidPath(const std::string_view s, bool is_pattern) { if (got_asterisk) { // * must be followed by a . (*.) // - // Viva Piñata: Party Animals (4D530819) has a bug in its game code where - // it attempts to FindFirstFile() with filters of "Game:\\*_X3.rkv", - // "Game:\\m*_X3.rkv", and "Game:\\w*_X3.rkv" and will infinite loop if - // the path filter is allowed. + // 4D530819 has a bug in its game code where it attempts to + // FindFirstFile() with filters of "Game:\\*_X3.rkv", "Game:\\m*_X3.rkv", + // and "Game:\\w*_X3.rkv" and will infinite loop if the path filter is + // allowed. if (c != '.') { return false; } @@ -674,6 +674,66 @@ dword_result_t FscSetCacheElementCount(dword_t unk_0, dword_t unk_1) { } DECLARE_XBOXKRNL_EXPORT1(FscSetCacheElementCount, kFileSystem, kStub); +dword_result_t NtDeviceIoControlFile( + dword_t handle, dword_t event_handle, dword_t apc_routine, + dword_t apc_context, dword_t io_status_block, dword_t io_control_code, + lpvoid_t input_buffer, dword_t input_buffer_len, lpvoid_t output_buffer, + dword_t output_buffer_len) { + // Called by XMountUtilityDrive cache-mounting code + // (checks if the returned values look valid, values below seem to pass the + // checks) + const uint32_t cache_size = 0xFF000; + + const uint32_t X_IOCTL_DISK_GET_DRIVE_GEOMETRY = 0x70000; + const uint32_t X_IOCTL_DISK_GET_PARTITION_INFO = 0x74004; + + if (io_control_code == X_IOCTL_DISK_GET_DRIVE_GEOMETRY) { + if (output_buffer_len < 0x8) { + assert_always(); + return X_STATUS_BUFFER_TOO_SMALL; + } + xe::store_and_swap(output_buffer, cache_size / 512); + xe::store_and_swap(output_buffer + 4, 512); + } else if (io_control_code == X_IOCTL_DISK_GET_PARTITION_INFO) { + if (output_buffer_len < 0x10) { + assert_always(); + return X_STATUS_BUFFER_TOO_SMALL; + } + xe::store_and_swap(output_buffer, 0); + xe::store_and_swap(output_buffer + 8, cache_size); + } else { + XELOGD("NtDeviceIoControlFile(0x{:X}) - unhandled IOCTL!", + uint32_t(io_control_code)); + assert_always(); + return X_STATUS_INVALID_PARAMETER; + } + + return X_STATUS_SUCCESS; +} +DECLARE_XBOXKRNL_EXPORT1(NtDeviceIoControlFile, kFileSystem, kStub); + +dword_result_t IoCreateDevice(dword_t device_struct, dword_t r4, dword_t r5, + dword_t r6, dword_t r7, lpdword_t out_struct) { + // Called from XMountUtilityDrive XAM-task code + // That code tries writing things to a pointer at out_struct+0x18 + // We'll alloc some scratch space for it so it doesn't cause any exceptions + + // 0x24 is guessed size from accesses to out_struct - likely incorrect + auto out_guest = kernel_memory()->SystemHeapAlloc(0x24); + + auto out = kernel_memory()->TranslateVirtual(out_guest); + memset(out, 0, 0x24); + + // XMountUtilityDrive writes some kind of header here + // 0x1000 bytes should be enough to store it + auto out_guest2 = kernel_memory()->SystemHeapAlloc(0x1000); + xe::store_and_swap(out + 0x18, out_guest2); + + *out_struct = out_guest; + return X_STATUS_SUCCESS; +} +DECLARE_XBOXKRNL_EXPORT1(IoCreateDevice, kFileSystem, kStub); + void RegisterIoExports(xe::cpu::ExportResolver* export_resolver, KernelState* kernel_state) {} diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_io_info.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_io_info.cc index 0add7390a..e43db31e6 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_io_info.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_io_info.cc @@ -126,6 +126,13 @@ dword_result_t NtQueryInformationFile( out_length = sizeof(*info); break; } + case XFileAlignmentInformation: { + // Requested by XMountUtilityDrive XAM-task + auto info = info_ptr.as(); + *info = 0; // FILE_BYTE_ALIGNMENT? + out_length = sizeof(*info); + break; + } default: { // Unsupported, for now. assert_always(); diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_module.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_module.cc index b2aafe294..f6b9846e0 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_module.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_module.cc @@ -189,6 +189,16 @@ XboxkrnlModule::XboxkrnlModule(Emulator* emulator, KernelState* kernel_state) ordinals::XexExecutableModuleHandle, ppXexExecutableModuleHandle); + // ExLoadedImageName (char*) + // The full path to loaded image/xex including its name. + // Used usually in custom dashboards (Aurora) + // Todo(Gliniak): Confirm that official kernel always allocate space for this + // variable. + uint32_t ppExLoadedImageName = + memory_->SystemHeapAlloc(kExLoadedImageNameSize); + export_resolver_->SetVariableMapping( + "xboxkrnl.exe", ordinals::ExLoadedImageName, ppExLoadedImageName); + // ExLoadedCommandLine (char*) // The name of the xex. Not sure this is ever really used on real devices. // Perhaps it's how swap disc/etc data is sent? diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_module.h b/src/xenia/kernel/xboxkrnl/xboxkrnl_module.h index 5a817453c..39a9c95b2 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_module.h +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_module.h @@ -27,6 +27,8 @@ namespace xboxkrnl { class XboxkrnlModule : public KernelModule { public: + static constexpr size_t kExLoadedImageNameSize = 255 + 1; + XboxkrnlModule(Emulator* emulator, KernelState* kernel_state); virtual ~XboxkrnlModule(); diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_strings.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_strings.cc index f350fd9e5..66a2141d7 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_strings.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_strings.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include "xenia/base/logging.h" #include "xenia/kernel/kernel_state.h" @@ -737,15 +738,15 @@ class StringFormatData : public FormatData { if (c >= 0x100) { return false; } - output_ << (char)c; + output_.push_back(char(c)); return true; } - std::string str() const { return output_.str(); } + const std::string& str() const { return output_; } private: const uint8_t* input_; - std::ostringstream output_; + std::string output_; }; class WideStringFormatData : public FormatData { @@ -771,15 +772,15 @@ class WideStringFormatData : public FormatData { } bool put(uint16_t c) { - output_ << (char16_t)c; + output_.push_back(char16_t(c)); return true; } - std::u16string wstr() const { return output_.str(); } + const std::u16string& wstr() const { return output_; } private: const uint16_t* input_; - std::basic_stringstream output_; + std::u16string output_; }; class WideCountFormatData : public FormatData { diff --git a/src/xenia/kernel/xobject.h b/src/xenia/kernel/xobject.h index 11d8ecb3c..67518b35c 100644 --- a/src/xenia/kernel/xobject.h +++ b/src/xenia/kernel/xobject.h @@ -111,7 +111,7 @@ struct X_OBJECT_TYPE { class XObject { public: - // Burnout Paradise needs proper handle value for certain calculations + // 45410806 needs proper handle value for certain calculations // It gets handle value from TLS (without base handle value is 0x88) // and substract 0xF8000088. Without base we're receiving wrong address // Instead of receiving address that starts with 0x82... we're receiving diff --git a/src/xenia/kernel/xsocket.cc b/src/xenia/kernel/xsocket.cc index ea15734e7..8bbc0375e 100644 --- a/src/xenia/kernel/xsocket.cc +++ b/src/xenia/kernel/xsocket.cc @@ -9,6 +9,8 @@ #include "src/xenia/kernel/xsocket.h" +#include + #include "xenia/base/platform.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/xam/xam_module.h" @@ -200,7 +202,7 @@ int XSocket::RecvFrom(uint8_t* buf, uint32_t buf_len, uint32_t flags, from->sin_family = nfrom.sin_family; from->sin_addr = ntohl(nfrom.sin_addr.s_addr); // BE <- BE from->sin_port = nfrom.sin_port; - memset(&from->sin_zero, 0, 8); + std::memset(from->x_sin_zero, 0, sizeof(from->x_sin_zero)); } if (from_len) { diff --git a/src/xenia/kernel/xsocket.h b/src/xenia/kernel/xsocket.h index 8c5103ec9..f77e2d8eb 100644 --- a/src/xenia/kernel/xsocket.h +++ b/src/xenia/kernel/xsocket.h @@ -10,6 +10,7 @@ #ifndef XENIA_KERNEL_XSOCKET_H_ #define XENIA_KERNEL_XSOCKET_H_ +#include #include #include "xenia/base/byte_order.h" @@ -42,7 +43,8 @@ struct XSOCKADDR_IN { // Always big-endian! xe::be sin_port; xe::be sin_addr; - char sin_zero[8]; + // sin_zero is defined as __pad on Android, so prefixed here. + char x_sin_zero[8]; }; // Xenia native sockaddr_in @@ -53,7 +55,7 @@ struct N_XSOCKADDR_IN { sin_family = other.sin_family; sin_port = other.sin_port; sin_addr = other.sin_addr; - std::memset(sin_zero, 0, 8); + std::memset(x_sin_zero, 0, sizeof(x_sin_zero)); return *this; } @@ -61,7 +63,8 @@ struct N_XSOCKADDR_IN { uint16_t sin_family; xe::be sin_port; xe::be sin_addr; - char sin_zero[8]; + // sin_zero is defined as __pad on Android, so prefixed here. + char x_sin_zero[8]; }; class XSocket : public XObject { 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/memory.cc b/src/xenia/memory.cc index 5b631e416..c7494e9fa 100644 --- a/src/xenia/memory.cc +++ b/src/xenia/memory.cc @@ -1531,12 +1531,11 @@ bool PhysicalHeap::Release(uint32_t base_address, uint32_t* out_region_size) { // Must invalidate here because the range being released may be reused in // another mapping of physical memory - but callback flags are set in each // heap separately (https://github.com/xenia-project/xenia/issues/1559 - - // dynamic vertices in Viva Pinata start screen and menu allocated in - // 0xA0000000 at addresses that overlap intro video textures in 0xE0000000, - // with the state of the allocator as of February 24th, 2020). If memory is - // invalidated in Alloc instead, Alloc won't be aware of callbacks enabled in - // other heaps, thus callback handlers will keep considering this range valid - // forever. + // dynamic vertices in 4D5307F2 start screen and menu allocated in 0xA0000000 + // at addresses that overlap intro video textures in 0xE0000000, with the + // state of the allocator as of February 24th, 2020). If memory is invalidated + // in Alloc instead, Alloc won't be aware of callbacks enabled in other heaps, + // thus callback handlers will keep considering this range valid forever. uint32_t region_size; if (QuerySize(base_address, ®ion_size)) { TriggerCallbacks(std::move(global_lock), base_address, region_size, true, 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 74b94ed7b..32e9d4bef 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 9ada2bb82..dca7ad408 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 CreateHostContext( 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 7b4daa770..4a1645d80 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 host-side graphics context and swapchain, possibly presenting // to a window and using the immediate drawer. virtual std::unique_ptr CreateHostContext( @@ -48,9 +45,7 @@ class GraphicsProvider { virtual std::unique_ptr CreateEmulationContext() = 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 4a753c853..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 { - -class PostedFn { - public: - explicit PostedFn(std::function fn) : fn_(std::move(fn)) {} - void Call() { fn_(); } - - private: - std::function fn_; -}; - -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) { - PostedFn* Fn = reinterpret_cast(posted_fn); - Fn->Call(); - return G_SOURCE_REMOVE; -} - -void GTKLoop::Post(std::function fn) { - assert_true(thread_id_ != std::thread::id()); - gdk_threads_add_idle(_posted_fn_thunk, - reinterpret_cast(new PostedFn(std::move(fn)))); -} - -void GTKLoop::PostDelayed(std::function fn, uint64_t delay_millis) { - gdk_threads_add_timeout( - delay_millis, _posted_fn_thunk, - reinterpret_cast(new PostedFn(std::move(fn)))); -} - -void GTKLoop::Quit() { assert_true(thread_id_ != std::thread::id()); } - -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 21a829d69..ca0a8a8c1 100644 --- a/src/xenia/ui/vulkan/premake5.lua +++ b/src/xenia/ui/vulkan/premake5.lua @@ -30,7 +30,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 974e543dc..1f974eedf 100644 --- a/src/xenia/ui/vulkan/vulkan_provider.cc +++ b/src/xenia/ui/vulkan/vulkan_provider.cc @@ -40,8 +40,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" @@ -57,9 +57,6 @@ std::unique_ptr VulkanProvider::Create(Window* main_window) { return provider; } -VulkanProvider::VulkanProvider(Window* main_window) - : GraphicsProvider(main_window) {} - VulkanProvider::~VulkanProvider() { for (size_t i = 0; i < size_t(HostSampler::kCount); ++i) { if (host_samplers_[i] != VK_NULL_HANDLE) { diff --git a/src/xenia/ui/vulkan/vulkan_provider.h b/src/xenia/ui/vulkan/vulkan_provider.h index 63114d1e0..cb3db6a4e 100644 --- a/src/xenia/ui/vulkan/vulkan_provider.h +++ b/src/xenia/ui/vulkan/vulkan_provider.h @@ -49,7 +49,7 @@ class VulkanProvider : public GraphicsProvider { public: ~VulkanProvider() override; - static std::unique_ptr Create(Window* main_window); + static std::unique_ptr Create(); std::unique_ptr CreateHostContext( Window* target_window) override; @@ -269,7 +269,7 @@ class VulkanProvider : public GraphicsProvider { } private: - 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 accdc65b9..2627c7ca0 100644 --- a/src/xenia/ui/window.h +++ b/src/xenia/ui/window.h @@ -14,11 +14,12 @@ #include #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 { @@ -30,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; @@ -68,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; } @@ -77,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()); } @@ -134,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); @@ -169,11 +177,19 @@ 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 + // GTK must have a default value here that isn't 0 + // TODO(Triang3l): Cleanup and unify this. May need to get the first resize + // message on various platforms. + int32_t width_ = 1280; + int32_t height_ = 720; +#else int32_t width_ = 0; int32_t height_ = 0; +#endif bool has_focus_ = true; bool is_cursor_visible_ = true; bool is_imgui_input_enabled_ = false; diff --git a/src/xenia/ui/window_demo.cc b/src/xenia/ui/window_demo.cc index 4e7f3ad06..727dbd33f 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 wtih the context (loading - // resources, etc). - graphics_provider = CreateDemoGraphicsProvider(window.get()); - window->set_context(graphics_provider->CreateHostContext(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_->CreateHostContext(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 9dbeccad5..b35f91305 100644 --- a/src/xenia/ui/window_gtk.cc +++ b/src/xenia/ui/window_gtk.cc @@ -7,9 +7,13 @@ ****************************************************************************** */ +#include #include +#include + #include "xenia/base/assert.h" +#include "xenia/base/clock.h" #include "xenia/base/logging.h" #include "xenia/base/platform_linux.h" #include "xenia/ui/virtual_key.h" @@ -18,33 +22,27 @@ namespace xe { namespace ui { -class FnWrapper { - public: - explicit FnWrapper(std::function fn) : fn_(std::move(fn)) {} - void Call() { fn_(); } - - private: - std::function fn_; -}; - -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(); if (window_) { - gtk_widget_destroy(window_); + if (GTK_IS_WIDGET(window_)) { + gtk_widget_destroy(window_); + } window_ = nullptr; } } bool GTKWindow::Initialize() { return OnCreate(); } -void gtk_event_handler_(GtkWidget* widget, GdkEvent* event, gpointer data) { +gboolean gtk_event_handler(GtkWidget* widget, GdkEvent* event, gpointer data) { GTKWindow* window = reinterpret_cast(data); switch (event->type) { case GDK_OWNER_CHANGE: @@ -58,34 +56,81 @@ void gtk_event_handler_(GtkWidget* widget, GdkEvent* event, gpointer data) { window->HandleKeyboard(&(event->key)); break; case GDK_SCROLL: - case GDK_BUTTON_PRESS: case GDK_MOTION_NOTIFY: + case GDK_BUTTON_PRESS: + case GDK_BUTTON_RELEASE: window->HandleMouse(&(event->any)); break; case GDK_FOCUS_CHANGE: window->HandleWindowFocus(&(event->focus_change)); break; case GDK_CONFIGURE: - window->HandleWindowResize(&(event->configure)); + // Only handle the event for the drawing area so we don't save + // a width and height that includes the menu bar on the full window + if (event->configure.window == + gtk_widget_get_window(window->drawing_area_)) { + window->HandleWindowResize(&(event->configure)); + } + break; default: // Do nothing - return; + break; } + // Propagate the event to other handlers + return GDK_EVENT_PROPAGATE; } -void GTKWindow::Create() { - window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); - gtk_window_set_title(GTK_WINDOW(window_), (gchar*)title_.c_str()); - gtk_window_set_default_size(GTK_WINDOW(window_), 1280, 720); - gtk_widget_show_all(window_); - g_signal_connect(G_OBJECT(window_), "destroy", G_CALLBACK(gtk_main_quit), - NULL); - g_signal_connect(G_OBJECT(window_), "event", G_CALLBACK(gtk_event_handler_), - reinterpret_cast(this)); +gboolean draw_callback(GtkWidget* widget, GdkFrameClock* frame_clock, + gpointer data) { + GTKWindow* window = reinterpret_cast(data); + window->HandleWindowPaint(); + return G_SOURCE_CONTINUE; +} + +gboolean close_callback(GtkWidget* widget, gpointer data) { + GTKWindow* window = reinterpret_cast(data); + window->Close(); + return G_SOURCE_CONTINUE; } bool GTKWindow::OnCreate() { - loop()->PostSynchronous([this]() { this->Create(); }); + // GTK optionally allows passing argv and argc here for parsing gtk specific + // options. We won't bother + window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_resizable(GTK_WINDOW(window_), true); + gtk_window_set_title(GTK_WINDOW(window_), title_.c_str()); + gtk_window_set_default_size(GTK_WINDOW(window_), width_, height_); + // Drawing area is where we will attach our vulkan/gl context + drawing_area_ = gtk_drawing_area_new(); + // tick callback is for the refresh rate of the window + gtk_widget_add_tick_callback(drawing_area_, draw_callback, + reinterpret_cast(this), nullptr); + // Attach our event handler to both the main window (for keystrokes) and the + // drawing area (for mouse input, resize event, etc) + g_signal_connect(G_OBJECT(drawing_area_), "event", + G_CALLBACK(gtk_event_handler), + reinterpret_cast(this)); + + GdkDisplay* gdk_display = gtk_widget_get_display(window_); + assert(GDK_IS_X11_DISPLAY(gdk_display)); + connection_ = XGetXCBConnection(gdk_x11_display_get_xdisplay(gdk_display)); + + g_signal_connect(G_OBJECT(window_), "event", G_CALLBACK(gtk_event_handler), + reinterpret_cast(this)); + // When the window manager kills the window (ie, the user hits X) + g_signal_connect(G_OBJECT(window_), "destroy", G_CALLBACK(close_callback), + reinterpret_cast(this)); + // Enable only keyboard events (so no mouse) for the top window + gtk_widget_set_events(window_, GDK_KEY_PRESS | GDK_KEY_RELEASE); + // Enable all events for the drawing area + gtk_widget_add_events(drawing_area_, GDK_ALL_EVENTS_MASK); + // Place the drawing area in a container (which later will hold the menu) + // then let it fill the whole area + box_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_box_pack_end(GTK_BOX(box_), drawing_area_, TRUE, TRUE, 0); + gtk_container_add(GTK_CONTAINER(window_), box_); + gtk_widget_show_all(window_); + return super::OnCreate(); } @@ -94,8 +139,6 @@ void GTKWindow::OnDestroy() { super::OnDestroy(); } void GTKWindow::OnClose() { if (!closing_ && window_) { closing_ = true; - gtk_widget_destroy(window_); - window_ = nullptr; } super::OnClose(); } @@ -123,7 +166,6 @@ void GTKWindow::ToggleFullscreen(bool fullscreen) { } fullscreen_ = fullscreen; - if (fullscreen) { gtk_window_fullscreen(GTK_WINDOW(window_)); } else { @@ -140,7 +182,6 @@ void GTKWindow::set_bordered(bool enabled) { // Don't screw with the borders if we're fullscreen. return; } - gtk_window_set_decorated(GTK_WINDOW(window_), enabled); } @@ -171,31 +212,30 @@ void GTKWindow::set_focus(bool value) { } void GTKWindow::Resize(int32_t width, int32_t height) { + if (is_fullscreen()) { + // Cannot resize while in fullscreen. + return; + } gtk_window_resize(GTK_WINDOW(window_), width, height); + super::Resize(width, height); } void GTKWindow::Resize(int32_t left, int32_t top, int32_t right, int32_t bottom) { - // TODO(dougvj) Verify that this is the desired behavior from this call + if (is_fullscreen()) { + // Cannot resize while in fullscreen. + return; + } gtk_window_move(GTK_WINDOW(window_), left, top); - gtk_window_resize(GTK_WINDOW(window_), left - right, top - bottom); + gtk_window_resize(GTK_WINDOW(window_), right - left, bottom - top); + super::Resize(left, top, right, bottom); } -void GTKWindow::OnResize(UIEvent* e) { - int32_t width; - int32_t height; - gtk_window_get_size(GTK_WINDOW(window_), &width, &height); - if (width != width_ || height != height_) { - width_ = width; - height_ = height; - Layout(); - } - super::OnResize(e); -} +void GTKWindow::OnResize(UIEvent* e) { super::OnResize(e); } void GTKWindow::Invalidate() { + // gtk_widget_queue_draw(drawing_area_); super::Invalidate(); - // TODO(dougvj) I am not sure what this is supposed to do } void GTKWindow::Close() { @@ -203,20 +243,22 @@ void GTKWindow::Close() { return; } closing_ = true; - Close(); OnClose(); + gtk_widget_destroy(window_); + window_ = nullptr; } void GTKWindow::OnMainMenuChange() { // We need to store the old handle for detachment - static GtkWidget* box = nullptr; + static int count = 0; auto main_menu = reinterpret_cast(main_menu_.get()); - if (main_menu && !is_fullscreen()) { - if (box) gtk_widget_destroy(box); - GtkWidget* menu = main_menu->handle(); - box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); - gtk_box_pack_start(GTK_BOX(box), menu, FALSE, FALSE, 3); - gtk_container_add(GTK_CONTAINER(window_), box); + if (main_menu && main_menu->handle()) { + if (!is_fullscreen()) { + gtk_box_pack_start(GTK_BOX(box_), main_menu->handle(), FALSE, FALSE, 0); + gtk_widget_show_all(window_); + } else { + gtk_container_remove(GTK_CONTAINER(box_), main_menu->handle()); + } } } @@ -234,9 +276,22 @@ bool GTKWindow::HandleWindowOwnerChange(GdkEventOwnerChange* event) { return false; } +bool GTKWindow::HandleWindowPaint() { + auto e = UIEvent(this); + OnPaint(&e); + return true; +} + bool GTKWindow::HandleWindowResize(GdkEventConfigure* event) { if (event->type == GDK_CONFIGURE) { + int32_t width = event->width; + int32_t height = event->height; auto e = UIEvent(this); + if (width != width_ || height != height_) { + width_ = width; + height_ = height; + Layout(); + } OnResize(&e); return true; } @@ -351,6 +406,7 @@ bool GTKWindow::HandleKeyboard(GdkEventKey* event) { bool ctrl_pressed = modifiers & GDK_CONTROL_MASK; bool alt_pressed = modifiers & GDK_META_MASK; bool super_pressed = modifiers & GDK_SUPER_MASK; + uint32_t key_char = gdk_keyval_to_unicode(event->keyval); // TODO(Triang3l): event->hardware_keycode to VirtualKey translation. auto e = KeyEvent(this, VirtualKey(event->hardware_keycode), 1, event->type == GDK_KEY_RELEASE, shift_pressed, ctrl_pressed, @@ -358,12 +414,13 @@ bool GTKWindow::HandleKeyboard(GdkEventKey* event) { switch (event->type) { case GDK_KEY_PRESS: OnKeyDown(&e); + if (key_char > 0) { + OnKeyChar(&e); + } break; case GDK_KEY_RELEASE: OnKeyUp(&e); break; - // TODO(dougvj) GDK doesn't have a KEY CHAR event, so we will have to - // figure out its equivalent here to call OnKeyChar(&e); default: return false; } @@ -377,23 +434,35 @@ std::unique_ptr MenuItem::Create(Type type, return std::make_unique(type, text, hotkey, callback); } -static void _menu_activate_callback(GtkWidget* menu, gpointer data) { - auto fn = reinterpret_cast(data); - fn->Call(); - delete fn; +static void _menu_activate_callback(GtkWidget* gtk_menu, gpointer data) { + GTKMenuItem* menu = reinterpret_cast(data); + menu->Activate(); +} + +void GTKMenuItem::Activate() { + try { + callback_(); + } catch (const std::bad_function_call& e) { + // Ignore + } } GTKMenuItem::GTKMenuItem(Type type, const std::string& text, const std::string& hotkey, std::function callback) : MenuItem(type, text, hotkey, std::move(callback)) { + std::string label = text; + // TODO(dougvj) Would we ever need to escape underscores? + // Replace & with _ for gtk to see the memonic + std::replace(label.begin(), label.end(), '&', '_'); + const gchar* gtk_label = reinterpret_cast(label.c_str()); switch (type) { case MenuItem::Type::kNormal: default: menu_ = gtk_menu_bar_new(); break; case MenuItem::Type::kPopup: - menu_ = gtk_menu_item_new_with_label((gchar*)text.c_str()); + menu_ = gtk_menu_item_new_with_mnemonic(gtk_label); break; case MenuItem::Type::kSeparator: menu_ = gtk_separator_menu_item_new(); @@ -403,12 +472,12 @@ GTKMenuItem::GTKMenuItem(Type type, const std::string& text, if (!hotkey.empty()) { full_name += "\t" + hotkey; } - menu_ = gtk_menu_item_new_with_label((gchar*)full_name.c_str()); + menu_ = gtk_menu_item_new_with_mnemonic(gtk_label); break; } if (GTK_IS_MENU_ITEM(menu_)) g_signal_connect(menu_, "activate", G_CALLBACK(_menu_activate_callback), - (gpointer) new FnWrapper(callback)); + (gpointer)this); } GTKMenuItem::~GTKMenuItem() { @@ -456,4 +525,5 @@ void GTKMenuItem::OnChildRemoved(MenuItem* generic_child_item) { } } // namespace ui + } // namespace xe diff --git a/src/xenia/ui/window_gtk.h b/src/xenia/ui/window_gtk.h index c20c0d5d6..6677f2fc7 100644 --- a/src/xenia/ui/window_gtk.h +++ b/src/xenia/ui/window_gtk.h @@ -15,6 +15,7 @@ #include #include +#include #include "xenia/base/platform_linux.h" #include "xenia/ui/menu_item.h" @@ -27,13 +28,14 @@ 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 { - return nullptr; + return connection_; } NativeWindowHandle native_handle() const override { return window_; } + GtkWidget* native_window_handle() const { return drawing_area_; } void EnableMainMenu() override {} void DisableMainMenu() override {} @@ -72,16 +74,23 @@ class GTKWindow : public Window { void OnResize(UIEvent* e) override; private: - void Create(); GtkWidget* window_; + GtkWidget* box_; + GtkWidget* drawing_area_; + xcb_connection_t* connection_; + + // C Callback shims for GTK + friend gboolean gtk_event_handler(GtkWidget*, GdkEvent*, gpointer); + friend gboolean close_callback(GtkWidget*, gpointer); + friend gboolean draw_callback(GtkWidget*, GdkFrameClock*, gpointer); - friend void gtk_event_handler_(GtkWidget*, GdkEvent*, gpointer); bool HandleMouse(GdkEventAny* event); bool HandleKeyboard(GdkEventKey* event); bool HandleWindowResize(GdkEventConfigure* event); bool HandleWindowFocus(GdkEventFocus* event); bool HandleWindowVisibility(GdkEventVisibility* event); bool HandleWindowOwnerChange(GdkEventOwnerChange* event); + bool HandleWindowPaint(); bool closing_ = false; bool fullscreen_ = false; @@ -95,6 +104,7 @@ class GTKMenuItem : public MenuItem { GtkWidget* handle() { return menu_; } using MenuItem::OnSelected; + void Activate(); void EnableMenuItem(Window& window) override {} void DisableMenuItem(Window& window) override {} diff --git a/src/xenia/ui/window_win.cc b/src/xenia/ui/window_win.cc index c5bfc780a..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); @@ -447,6 +456,7 @@ void Win32Window::Close() { closing_ = true; OnClose(); DestroyWindow(hwnd_); + hwnd_ = nullptr; } void Win32Window::OnMainMenuChange() { 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/devices/null_device.cc b/src/xenia/vfs/devices/null_device.cc new file mode 100644 index 000000000..e7b014d7b --- /dev/null +++ b/src/xenia/vfs/devices/null_device.cc @@ -0,0 +1,62 @@ +/** + ****************************************************************************** + * 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/vfs/devices/null_device.h" + +#include "xenia/base/filesystem.h" +#include "xenia/base/logging.h" +#include "xenia/base/math.h" +#include "xenia/kernel/xfile.h" +#include "xenia/vfs/devices/null_entry.h" + +namespace xe { +namespace vfs { + +NullDevice::NullDevice(const std::string& mount_path, + const std::initializer_list& null_paths) + : Device(mount_path), null_paths_(null_paths), name_("NullDevice") {} + +NullDevice::~NullDevice() = default; + +bool NullDevice::Initialize() { + auto root_entry = new NullEntry(this, nullptr, mount_path_); + root_entry->attributes_ = kFileAttributeDirectory; + root_entry_ = std::unique_ptr(root_entry); + + for (auto path : null_paths_) { + auto child = NullEntry::Create(this, root_entry, path); + root_entry->children_.push_back(std::unique_ptr(child)); + } + return true; +} + +void NullDevice::Dump(StringBuffer* string_buffer) { + auto global_lock = global_critical_region_.Acquire(); + root_entry_->Dump(string_buffer, 0); +} + +Entry* NullDevice::ResolvePath(const std::string_view path) { + XELOGFS("NullDevice::ResolvePath({})", path); + + auto root = root_entry_.get(); + if (path.empty()) { + return root_entry_.get(); + } + + for (auto& child : root->children()) { + if (!strcasecmp(child->path().c_str(), path.data())) { + return child.get(); + } + } + + return nullptr; +} + +} // namespace vfs +} // namespace xe diff --git a/src/xenia/vfs/devices/null_device.h b/src/xenia/vfs/devices/null_device.h new file mode 100644 index 000000000..6ac8f2a35 --- /dev/null +++ b/src/xenia/vfs/devices/null_device.h @@ -0,0 +1,57 @@ +/** + ****************************************************************************** + * 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_VFS_DEVICES_NULL_DEVICE_H_ +#define XENIA_VFS_DEVICES_NULL_DEVICE_H_ + +#include + +#include "xenia/vfs/device.h" + +namespace xe { +namespace vfs { + +class NullEntry; + +class NullDevice : public Device { + public: + NullDevice(const std::string& mount_path, + const std::initializer_list& null_paths); + ~NullDevice() override; + + bool Initialize() override; + void Dump(StringBuffer* string_buffer) override; + Entry* ResolvePath(const std::string_view path) override; + + bool is_read_only() const override { return false; } + + const std::string& name() const override { return name_; } + uint32_t attributes() const override { return 0; } + uint32_t component_name_max_length() const override { return 40; } + + uint32_t total_allocation_units() const override { return 0x10; } + uint32_t available_allocation_units() const override { return 0x10; } + + // STFC/cache code seems to require the product of the next two to equal + // 0x10000 + uint32_t sectors_per_allocation_unit() const override { return 0x80; } + + // STFC requires <= 0x1000 + uint32_t bytes_per_sector() const override { return 0x200; } + + private: + std::string name_; + std::unique_ptr root_entry_; + std::vector null_paths_; +}; + +} // namespace vfs +} // namespace xe + +#endif // XENIA_VFS_DEVICES_NULL_DEVICE_H_ diff --git a/src/xenia/vfs/devices/null_entry.cc b/src/xenia/vfs/devices/null_entry.cc new file mode 100644 index 000000000..45b511c40 --- /dev/null +++ b/src/xenia/vfs/devices/null_entry.cc @@ -0,0 +1,55 @@ +/** + ****************************************************************************** + * 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/vfs/devices/null_entry.h" + +#include "xenia/base/filesystem.h" +#include "xenia/base/logging.h" +#include "xenia/base/mapped_memory.h" +#include "xenia/base/math.h" +#include "xenia/base/string.h" +#include "xenia/vfs/device.h" +#include "xenia/vfs/devices/null_file.h" + +namespace xe { +namespace vfs { + +NullEntry::NullEntry(Device* device, Entry* parent, std::string path) + : Entry(device, parent, path) {} + +NullEntry::~NullEntry() = default; + +NullEntry* NullEntry::Create(Device* device, Entry* parent, + const std::string& path) { + auto entry = new NullEntry(device, parent, path); + + entry->create_timestamp_ = 0; + entry->access_timestamp_ = 0; + entry->write_timestamp_ = 0; + + entry->attributes_ = kFileAttributeNormal; + + entry->size_ = 0; + entry->allocation_size_ = 0; + return entry; +} + +X_STATUS NullEntry::Open(uint32_t desired_access, File** out_file) { + if (is_read_only() && (desired_access & (FileAccess::kFileWriteData | + FileAccess::kFileAppendData))) { + XELOGE("Attempting to open file for write access on read-only device"); + return X_STATUS_ACCESS_DENIED; + } + + *out_file = new NullFile(desired_access, this); + return X_STATUS_SUCCESS; +} + +} // namespace vfs +} // namespace xe diff --git a/src/xenia/vfs/devices/null_entry.h b/src/xenia/vfs/devices/null_entry.h new file mode 100644 index 000000000..84f01cb95 --- /dev/null +++ b/src/xenia/vfs/devices/null_entry.h @@ -0,0 +1,42 @@ +/** + ****************************************************************************** + * 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_VFS_DEVICES_NULL_ENTRY_H_ +#define XENIA_VFS_DEVICES_NULL_ENTRY_H_ + +#include + +#include "xenia/base/filesystem.h" +#include "xenia/vfs/entry.h" + +namespace xe { +namespace vfs { + +class NullDevice; + +class NullEntry : public Entry { + public: + NullEntry(Device* device, Entry* parent, std::string path); + ~NullEntry() override; + + static NullEntry* Create(Device* device, Entry* parent, + const std::string& path); + + X_STATUS Open(uint32_t desired_access, File** out_file) override; + + bool can_map() const override { return false; } + + private: + friend class NullDevice; +}; + +} // namespace vfs +} // namespace xe + +#endif // XENIA_VFS_DEVICES_NULL_ENTRY_H_ diff --git a/src/xenia/vfs/devices/null_file.cc b/src/xenia/vfs/devices/null_file.cc new file mode 100644 index 000000000..e04fd8397 --- /dev/null +++ b/src/xenia/vfs/devices/null_file.cc @@ -0,0 +1,52 @@ +/** + ****************************************************************************** + * 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/vfs/devices/null_file.h" + +#include "xenia/vfs/devices/null_entry.h" + +namespace xe { +namespace vfs { + +NullFile::NullFile(uint32_t file_access, NullEntry* entry) + : File(file_access, entry) {} + +NullFile::~NullFile() = default; + +void NullFile::Destroy() { delete this; } + +X_STATUS NullFile::ReadSync(void* buffer, size_t buffer_length, + size_t byte_offset, size_t* out_bytes_read) { + if (!(file_access_ & FileAccess::kFileReadData)) { + return X_STATUS_ACCESS_DENIED; + } + + return X_STATUS_SUCCESS; +} + +X_STATUS NullFile::WriteSync(const void* buffer, size_t buffer_length, + size_t byte_offset, size_t* out_bytes_written) { + if (!(file_access_ & + (FileAccess::kFileWriteData | FileAccess::kFileAppendData))) { + return X_STATUS_ACCESS_DENIED; + } + + return X_STATUS_SUCCESS; +} + +X_STATUS NullFile::SetLength(size_t length) { + if (!(file_access_ & FileAccess::kFileWriteData)) { + return X_STATUS_ACCESS_DENIED; + } + + return X_STATUS_SUCCESS; +} + +} // namespace vfs +} // namespace xe diff --git a/src/xenia/vfs/devices/null_file.h b/src/xenia/vfs/devices/null_file.h new file mode 100644 index 000000000..32a6be25c --- /dev/null +++ b/src/xenia/vfs/devices/null_file.h @@ -0,0 +1,40 @@ +/** + ****************************************************************************** + * 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_VFS_DEVICES_NULL_FILE_H_ +#define XENIA_VFS_DEVICES_NULL_FILE_H_ + +#include + +#include "xenia/base/filesystem.h" +#include "xenia/vfs/file.h" + +namespace xe { +namespace vfs { + +class NullEntry; + +class NullFile : public File { + public: + NullFile(uint32_t file_access, NullEntry* entry); + ~NullFile() override; + + void Destroy() override; + + X_STATUS ReadSync(void* buffer, size_t buffer_length, size_t byte_offset, + size_t* out_bytes_read) override; + X_STATUS WriteSync(const void* buffer, size_t buffer_length, + size_t byte_offset, size_t* out_bytes_written) override; + X_STATUS SetLength(size_t length) override; +}; + +} // namespace vfs +} // namespace xe + +#endif // XENIA_VFS_DEVICES_NULL_FILE_H_ 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/third_party/SDL2 b/third_party/SDL2 index d28437de3..ea9bece5e 160000 --- a/third_party/SDL2 +++ b/third_party/SDL2 @@ -1 +1 @@ -Subproject commit d28437de3c74d2514fc96d4e0da1dec4113e8929 +Subproject commit ea9bece5ed4e76b6636d941101923487a6378ca6 diff --git a/third_party/SDL2-static.lua b/third_party/SDL2-static.lua index d30f914b3..492c0d3d9 100644 --- a/third_party/SDL2-static.lua +++ b/third_party/SDL2-static.lua @@ -50,6 +50,7 @@ project("SDL2") "SDL2/include/SDL_log.h", "SDL2/include/SDL_main.h", "SDL2/include/SDL_messagebox.h", + "SDL2/include/SDL_metal.h", "SDL2/include/SDL_misc.h", "SDL2/include/SDL_mouse.h", "SDL2/include/SDL_mutex.h", @@ -88,6 +89,7 @@ project("SDL2") "SDL2/include/SDL_test_images.h", "SDL2/include/SDL_test_log.h", "SDL2/include/SDL_test_md5.h", + "SDL2/include/SDL_test_memory.h", "SDL2/include/SDL_test_random.h", "SDL2/include/SDL_thread.h", "SDL2/include/SDL_timer.h", @@ -114,6 +116,7 @@ project("SDL2") "SDL2/src/dynapi/SDL_dynapi_procs.h", "SDL2/src/events/blank_cursor.h", "SDL2/src/events/default_cursor.h", + "SDL2/src/events/scancodes_windows.h", "SDL2/src/events/SDL_clipboardevents_c.h", "SDL2/src/events/SDL_displayevents_c.h", "SDL2/src/events/SDL_dropevents_c.h", @@ -124,16 +127,20 @@ project("SDL2") "SDL2/src/events/SDL_sysevents.h", "SDL2/src/events/SDL_touch_c.h", "SDL2/src/events/SDL_windowevents_c.h", + "SDL2/src/haptic/SDL_haptic_c.h", "SDL2/src/haptic/SDL_syshaptic.h", "SDL2/src/haptic/windows/SDL_dinputhaptic_c.h", "SDL2/src/haptic/windows/SDL_windowshaptic_c.h", "SDL2/src/haptic/windows/SDL_xinputhaptic_c.h", "SDL2/src/hidapi/hidapi/hidapi.h", + "SDL2/src/hidapi/SDL_hidapi.h", "SDL2/src/joystick/controller_type.h", "SDL2/src/joystick/hidapi/SDL_hidapijoystick_c.h", + "SDL2/src/joystick/hidapi/SDL_hidapi_rumble.h", "SDL2/src/joystick/SDL_gamecontrollerdb.h", "SDL2/src/joystick/SDL_joystick_c.h", "SDL2/src/joystick/SDL_sysjoystick.h", + "SDL2/src/joystick/usb_ids.h", "SDL2/src/joystick/virtual/SDL_virtualjoystick_c.h", "SDL2/src/joystick/windows/SDL_dinputjoystick_c.h", "SDL2/src/joystick/windows/SDL_rawinputjoystick_c.h", @@ -142,11 +149,14 @@ project("SDL2") "SDL2/src/libm/math_libm.h", "SDL2/src/libm/math_private.h", "SDL2/src/locale/SDL_syslocale.h", + "SDL2/src/misc/SDL_sysurl.h", + "SDL2/src/power/SDL_syspower.h", "SDL2/src/render/direct3d11/SDL_shaders_d3d11.h", "SDL2/src/render/direct3d/SDL_shaders_d3d.h", + "SDL2/src/render/opengles2/SDL_gles2funcs.h", + "SDL2/src/render/opengles2/SDL_shaders_gles2.h", "SDL2/src/render/opengl/SDL_glfuncs.h", "SDL2/src/render/opengl/SDL_shaders_gl.h", - "SDL2/src/render/opengles/SDL_glesfuncs.h", "SDL2/src/render/SDL_d3dmath.h", "SDL2/src/render/SDL_sysrender.h", "SDL2/src/render/SDL_yuv_sw_c.h", @@ -158,8 +168,11 @@ project("SDL2") "SDL2/src/render/software/SDL_drawpoint.h", "SDL2/src/render/software/SDL_render_sw_c.h", "SDL2/src/render/software/SDL_rotate.h", + "SDL2/src/SDL_assert_c.h", "SDL2/src/SDL_dataqueue.h", "SDL2/src/SDL_error_c.h", + "SDL2/src/SDL_hints_c.h", + "SDL2/src/SDL_internal.h", "SDL2/src/sensor/dummy/SDL_dummysensor.h", "SDL2/src/sensor/SDL_sensor_c.h", "SDL2/src/sensor/SDL_syssensor.h", @@ -173,10 +186,32 @@ project("SDL2") "SDL2/src/video/dummy/SDL_nullevents_c.h", "SDL2/src/video/dummy/SDL_nullframebuffer_c.h", "SDL2/src/video/dummy/SDL_nullvideo.h", + "SDL2/src/video/khronos/vulkan/vk_icd.h", + "SDL2/src/video/khronos/vulkan/vk_layer.h", + "SDL2/src/video/khronos/vulkan/vk_platform.h", + "SDL2/src/video/khronos/vulkan/vk_sdk_platform.h", + "SDL2/src/video/khronos/vulkan/vulkan.h", + "SDL2/src/video/khronos/vulkan/vulkan.hpp", + "SDL2/src/video/khronos/vulkan/vulkan_android.h", + "SDL2/src/video/khronos/vulkan/vulkan_beta.h", + "SDL2/src/video/khronos/vulkan/vulkan_core.h", + "SDL2/src/video/khronos/vulkan/vulkan_directfb.h", + "SDL2/src/video/khronos/vulkan/vulkan_fuchsia.h", + "SDL2/src/video/khronos/vulkan/vulkan_ggp.h", + "SDL2/src/video/khronos/vulkan/vulkan_ios.h", + "SDL2/src/video/khronos/vulkan/vulkan_macos.h", + "SDL2/src/video/khronos/vulkan/vulkan_metal.h", + "SDL2/src/video/khronos/vulkan/vulkan_vi.h", + "SDL2/src/video/khronos/vulkan/vulkan_wayland.h", + "SDL2/src/video/khronos/vulkan/vulkan_win32.h", + "SDL2/src/video/khronos/vulkan/vulkan_xcb.h", + "SDL2/src/video/khronos/vulkan/vulkan_xlib.h", + "SDL2/src/video/khronos/vulkan/vulkan_xlib_xrandr.h", "SDL2/src/video/SDL_blit.h", "SDL2/src/video/SDL_blit_auto.h", "SDL2/src/video/SDL_blit_copy.h", "SDL2/src/video/SDL_blit_slow.h", + "SDL2/src/video/SDL_egl_c.h", "SDL2/src/video/SDL_pixels_c.h", "SDL2/src/video/SDL_rect_c.h", "SDL2/src/video/SDL_RLEaccel_c.h", @@ -184,6 +219,7 @@ project("SDL2") "SDL2/src/video/SDL_sysvideo.h", "SDL2/src/video/SDL_vulkan_internal.h", "SDL2/src/video/SDL_yuv_c.h", + "SDL2/src/video/windows/SDL_msctf.h", "SDL2/src/video/windows/SDL_vkeys.h", "SDL2/src/video/windows/SDL_windowsclipboard.h", "SDL2/src/video/windows/SDL_windowsevents.h", @@ -193,12 +229,16 @@ project("SDL2") "SDL2/src/video/windows/SDL_windowsmodes.h", "SDL2/src/video/windows/SDL_windowsmouse.h", "SDL2/src/video/windows/SDL_windowsopengl.h", + "SDL2/src/video/windows/SDL_windowsopengles.h", "SDL2/src/video/windows/SDL_windowsshape.h", + "SDL2/src/video/windows/SDL_windowstaskdialog.h", "SDL2/src/video/windows/SDL_windowsvideo.h", "SDL2/src/video/windows/SDL_windowsvulkan.h", "SDL2/src/video/windows/SDL_windowswindow.h", "SDL2/src/video/windows/wmmsg.h", "SDL2/src/video/yuv2rgb/yuv_rgb.h", + "SDL2/src/video/yuv2rgb/yuv_rgb_sse_func.h", + "SDL2/src/video/yuv2rgb/yuv_rgb_std_func.h", "SDL2/src/atomic/SDL_atomic.c", "SDL2/src/atomic/SDL_spinlock.c", @@ -240,6 +280,7 @@ project("SDL2") "SDL2/src/joystick/dummy/SDL_sysjoystick.c", "SDL2/src/joystick/hidapi/SDL_hidapijoystick.c", "SDL2/src/joystick/hidapi/SDL_hidapi_gamecube.c", + "SDL2/src/joystick/hidapi/SDL_hidapi_luna.c", "SDL2/src/joystick/hidapi/SDL_hidapi_ps4.c", "SDL2/src/joystick/hidapi/SDL_hidapi_ps5.c", "SDL2/src/joystick/hidapi/SDL_hidapi_rumble.c", diff --git a/tools/build/scripts/pkg_config.lua b/tools/build/scripts/pkg_config.lua index 9e565de57..2042491b9 100644 --- a/tools/build/scripts/pkg_config.lua +++ b/tools/build/scripts/pkg_config.lua @@ -3,7 +3,7 @@ pkg_config = {} function pkg_config.cflags(lib) - if os.istarget("windows") then + if not os.istarget("linux") then return end buildoptions({ @@ -12,7 +12,7 @@ function pkg_config.cflags(lib) end function pkg_config.lflags(lib) - if os.istarget("windows") then + if not os.istarget("linux") then return end linkoptions({ @@ -24,7 +24,9 @@ function pkg_config.lflags(lib) local output = ({os.outputof("pkg-config --libs-only-l "..lib)})[1] for k, flag in next, string.explode(output, " ") do -- remove "-l" - links(string.sub(flag, 3)) + if flag ~= "" then + links(string.sub(flag, 3)) + end end end 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); diff --git a/xenia-build b/xenia-build index 6e1547990..9dbb32c6d 100755 --- a/xenia-build +++ b/xenia-build @@ -262,13 +262,31 @@ def generate_version_h(): """Generates a build/version.h file that contains current git info. """ header_file = 'build/version.h' - if git_is_repository(): + pr_number = 0 + pr_repo_name = "" + pr_branch_name = "" + pr_commit = "" + pr_commit_short = "" + if os.getenv('APPVEYOR') == 'True': + branch_name = os.getenv('APPVEYOR_REPO_BRANCH') + commit = os.getenv('APPVEYOR_REPO_COMMIT') + commit_short = commit[:9] + pr_number = os.getenv('APPVEYOR_PULL_REQUEST_NUMBER') + if not pr_number: + pr_number = 0 + else: + pr_repo_name = os.getenv('APPVEYOR_PULL_REQUEST_HEAD_REPO_NAME') + pr_branch_name = os.getenv('APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH') + pr_commit = os.getenv('APPVEYOR_PULL_REQUEST_HEAD_COMMIT') + pr_commit_short = pr_commit[:9] + elif git_is_repository(): (branch_name, commit, commit_short) = git_get_head_info() else: branch_name = 'tarball' commit = ':(-dont-do-this' commit_short = ':(' + # header contents_new = '''// Autogenerated by `xb premake`. #ifndef GENERATED_VERSION_H_ #define GENERATED_VERSION_H_ @@ -276,9 +294,22 @@ def generate_version_h(): #define XE_BUILD_COMMIT "{}" #define XE_BUILD_COMMIT_SHORT "{}" #define XE_BUILD_DATE __DATE__ -#endif // GENERATED_VERSION_H_ '''.format(branch_name, commit, commit_short) + # PR info (if available) + if pr_number != 0: + contents_new += '''#define XE_BUILD_IS_PR +#define XE_BUILD_PR_NUMBER "{}" +#define XE_BUILD_PR_REPO "{}" +#define XE_BUILD_PR_BRANCH "{}" +#define XE_BUILD_PR_COMMIT "{}" +#define XE_BUILD_PR_COMMIT_SHORT "{}" +'''.format(pr_number, pr_repo_name, pr_branch_name, pr_commit, pr_commit_short) + + # footer + contents_new += '''#endif // GENERATED_VERSION_H_ +''' + contents_old = None if os.path.exists(header_file) and os.path.getsize(header_file) < 1024: with open(header_file, 'r') as f: