Merge branch 'master' into vulkan

This commit is contained in:
Triang3l
2021-09-12 14:10:36 +03:00
161 changed files with 3359 additions and 1562 deletions

View File

@@ -47,8 +47,8 @@ bool D3D12Provider::IsD3D12APIAvailable() {
return true;
}
std::unique_ptr<D3D12Provider> D3D12Provider::Create(Window* main_window) {
std::unique_ptr<D3D12Provider> provider(new D3D12Provider(main_window));
std::unique_ptr<D3D12Provider> D3D12Provider::Create() {
std::unique_ptr<D3D12Provider> provider(new D3D12Provider);
if (!provider->Initialize()) {
xe::FatalError(
"Unable to initialize Direct3D 12 graphics subsystem.\n"
@@ -63,9 +63,6 @@ std::unique_ptr<D3D12Provider> D3D12Provider::Create(Window* main_window) {
return provider;
}
D3D12Provider::D3D12Provider(Window* main_window)
: GraphicsProvider(main_window) {}
D3D12Provider::~D3D12Provider() {
if (graphics_analysis_ != nullptr) {
graphics_analysis_->Release();

View File

@@ -27,7 +27,7 @@ class D3D12Provider : public GraphicsProvider {
static bool IsD3D12APIAvailable();
static std::unique_ptr<D3D12Provider> Create(Window* main_window);
static std::unique_ptr<D3D12Provider> Create();
std::unique_ptr<GraphicsContext> 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();

View File

@@ -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 <memory>
#include <string>
#include <vector>
#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<std::string>& args);
class D3D12WindowDemoApp final : public WindowDemoApp {
public:
static std::unique_ptr<WindowedApp> Create(WindowedAppContext& app_context) {
return std::unique_ptr<WindowedApp>(new D3D12WindowDemoApp(app_context));
}
std::unique_ptr<GraphicsProvider> CreateDemoGraphicsProvider(Window* window) {
return xe::ui::d3d12::D3D12Provider::Create(window);
protected:
std::unique_ptr<GraphicsProvider> CreateGraphicsProvider() const override;
private:
explicit D3D12WindowDemoApp(WindowedAppContext& app_context)
: WindowDemoApp(app_context, "xenia-ui-window-d3d12-demo") {}
};
std::unique_ptr<GraphicsProvider> 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);

View File

@@ -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,

View File

@@ -12,6 +12,9 @@
#include "xenia/base/string.h"
#include "xenia/ui/file_picker.h"
// Microsoft headers after platform_win.h.
#include <wrl/client.h>
namespace xe {
namespace ui {
@@ -106,32 +109,16 @@ Win32FilePicker::Win32FilePicker() = default;
Win32FilePicker::~Win32FilePicker() = default;
template <typename T>
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<IFileDialog> file_dialog;
Microsoft::WRL::ComPtr<IFileDialog> 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<IFileDialogEvents> file_dialog_events;
hr = CDialogEventHandler_CreateInstance(
IID_PPV_ARGS(file_dialog_events.addressof()));
Microsoft::WRL::ComPtr<IFileDialogEvents> 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<IShellItem> shell_item;
hr = file_dialog->GetResult(shell_item.addressof());
Microsoft::WRL::ComPtr<IShellItem> shell_item;
hr = file_dialog->GetResult(&shell_item);
if (!SUCCEEDED(hr)) {
return false;
}

View File

@@ -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<GraphicsContext> CreateHostContext(
@@ -48,9 +45,7 @@ class GraphicsProvider {
virtual std::unique_ptr<GraphicsContext> CreateEmulationContext() = 0;
protected:
explicit GraphicsProvider(Window* main_window) : main_window_(main_window) {}
Window* main_window_ = nullptr;
GraphicsProvider() = default;
};
} // namespace ui

View File

@@ -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<void()> 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

View File

@@ -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 <functional>
#include <memory>
#include "xenia/base/delegate.h"
#include "xenia/ui/ui_event.h"
namespace xe {
namespace ui {
class Loop {
public:
static std::unique_ptr<Loop> 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<void()> fn) = 0;
virtual void PostDelayed(std::function<void()> fn, uint64_t delay_millis) = 0;
void PostSynchronous(std::function<void()> fn);
virtual void Quit() = 0;
virtual void AwaitQuit() = 0;
Delegate<UIEvent*> on_quit;
};
} // namespace ui
} // namespace xe
#endif // XENIA_UI_LOOP_H_

View File

@@ -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 <gdk/gdkx.h>
#include <gtk/gtk.h>
#include "xenia/base/assert.h"
namespace xe {
namespace ui {
class PostedFn {
public:
explicit PostedFn(std::function<void()> fn) : fn_(std::move(fn)) {}
void Call() { fn_(); }
private:
std::function<void()> fn_;
};
std::unique_ptr<Loop> Loop::Create() { return std::make_unique<GTKLoop>(); }
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<PostedFn*>(posted_fn);
Fn->Call();
return G_SOURCE_REMOVE;
}
void GTKLoop::Post(std::function<void()> fn) {
assert_true(thread_id_ != std::thread::id());
gdk_threads_add_idle(_posted_fn_thunk,
reinterpret_cast<gpointer>(new PostedFn(std::move(fn))));
}
void GTKLoop::PostDelayed(std::function<void()> fn, uint64_t delay_millis) {
gdk_threads_add_timeout(
delay_millis, _posted_fn_thunk,
reinterpret_cast<gpointer>(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

View File

@@ -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> Loop::Create() { return std::make_unique<Win32Loop>(); }
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<std::mutex> 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<std::recursive_mutex> 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<void()> fn) {
assert_true(thread_id_ != 0);
{
std::lock_guard<std::recursive_mutex> 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<PendingTimer*>(context);
auto loop = timer->loop;
auto fn = std::move(timer->fn);
DeleteTimerQueueTimer(timer->timer_queue, timer->timer_handle, NULL);
{
std::lock_guard<std::mutex> lock(loop->pending_timers_mutex_);
loop->pending_timers_.remove(timer);
}
delete timer;
loop->Post(std::move(fn));
}
void Win32Loop::PostDelayed(std::function<void()> 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<std::mutex> 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

View File

@@ -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 <list>
#include <mutex>
#include <thread>
#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<void()> fn) override;
void PostDelayed(std::function<void()> 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<void()> fn;
};
class PostedFn {
public:
explicit PostedFn(std::function<void()> fn) : fn_(std::move(fn)) {}
void Call() { fn_(); }
private:
std::function<void()> 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<PendingTimer*> pending_timers_;
std::recursive_mutex posted_functions_mutex_;
std::list<PostedFn> posted_functions_;
};
} // namespace ui
} // namespace xe
#endif // XENIA_UI_LOOP_WIN_H_

View File

@@ -13,3 +13,4 @@ project("xenia-ui")
})
local_platform_files()
removefiles({"*_demo.cc"})
removefiles({"windowed_app_main_*.cc"})

View File

@@ -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,

View File

@@ -40,8 +40,8 @@ namespace xe {
namespace ui {
namespace vulkan {
std::unique_ptr<VulkanProvider> VulkanProvider::Create(Window* main_window) {
std::unique_ptr<VulkanProvider> provider(new VulkanProvider(main_window));
std::unique_ptr<VulkanProvider> VulkanProvider::Create() {
std::unique_ptr<VulkanProvider> provider(new VulkanProvider);
if (!provider->Initialize()) {
xe::FatalError(
"Unable to initialize Vulkan graphics subsystem.\n"
@@ -57,9 +57,6 @@ std::unique_ptr<VulkanProvider> 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) {

View File

@@ -49,7 +49,7 @@ class VulkanProvider : public GraphicsProvider {
public:
~VulkanProvider() override;
static std::unique_ptr<VulkanProvider> Create(Window* main_window);
static std::unique_ptr<VulkanProvider> Create();
std::unique_ptr<GraphicsContext> CreateHostContext(
Window* target_window) override;
@@ -269,7 +269,7 @@ class VulkanProvider : public GraphicsProvider {
}
private:
explicit VulkanProvider(Window* main_window);
VulkanProvider() = default;
bool Initialize();

View File

@@ -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 <memory>
#include <string>
#include <vector>
#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<std::string>& args);
class VulkanWindowDemoApp final : public WindowDemoApp {
public:
static std::unique_ptr<WindowedApp> Create(WindowedAppContext& app_context) {
return std::unique_ptr<WindowedApp>(new VulkanWindowDemoApp(app_context));
}
std::unique_ptr<GraphicsProvider> CreateDemoGraphicsProvider(Window* window) {
return xe::ui::vulkan::VulkanProvider::Create(window);
protected:
std::unique_ptr<GraphicsProvider> CreateGraphicsProvider() const override;
private:
explicit VulkanWindowDemoApp(WindowedAppContext& app_context)
: WindowDemoApp(app_context, "xenia-ui-window-vulkan-demo") {}
};
std::unique_ptr<GraphicsProvider> 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);

View File

@@ -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.

View File

@@ -14,11 +14,12 @@
#include <string>
#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<Window> Create(Loop* loop, const std::string& title);
static std::unique_ptr<Window> 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<MouseEvent*> on_mouse_wheel;
protected:
Window(Loop* loop, const std::string& title);
Window(WindowedAppContext& app_context, const std::string& title);
void ForEachListener(std::function<void(WindowListener*)> fn);
void TryForEachListener(std::function<bool(WindowListener*)> 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<MenuItem> 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;

View File

@@ -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<GraphicsProvider> CreateDemoGraphicsProvider(Window* window);
WindowDemoApp::~WindowDemoApp() { Profiler::Shutdown(); }
int window_demo_main(const std::vector<std::string>& 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<std::string>& 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<std::string>& 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<GraphicsProvider> 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<std::string>& 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

View File

@@ -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 <memory>
#include <string>
#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<GraphicsProvider> CreateGraphicsProvider() const = 0;
private:
std::unique_ptr<GraphicsProvider> graphics_provider_;
std::unique_ptr<Window> window_;
};
} // namespace ui
} // namespace xe
#endif // XENIA_UI_WINDOW_DEMO_H_

View File

@@ -7,9 +7,13 @@
******************************************************************************
*/
#include <algorithm>
#include <string>
#include <X11/Xlib-xcb.h>
#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<void()> fn) : fn_(std::move(fn)) {}
void Call() { fn_(); }
private:
std::function<void()> fn_;
};
std::unique_ptr<Window> Window::Create(Loop* loop, const std::string& title) {
return std::make_unique<GTKWindow>(loop, title);
std::unique_ptr<Window> Window::Create(WindowedAppContext& app_context,
const std::string& title) {
return std::make_unique<GTKWindow>(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<GTKWindow*>(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<gpointer>(this));
gboolean draw_callback(GtkWidget* widget, GdkFrameClock* frame_clock,
gpointer data) {
GTKWindow* window = reinterpret_cast<GTKWindow*>(data);
window->HandleWindowPaint();
return G_SOURCE_CONTINUE;
}
gboolean close_callback(GtkWidget* widget, gpointer data) {
GTKWindow* window = reinterpret_cast<GTKWindow*>(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<gpointer>(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<gpointer>(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<gpointer>(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<gpointer>(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<GTKMenuItem*>(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<ui::MenuItem> MenuItem::Create(Type type,
return std::make_unique<GTKMenuItem>(type, text, hotkey, callback);
}
static void _menu_activate_callback(GtkWidget* menu, gpointer data) {
auto fn = reinterpret_cast<FnWrapper*>(data);
fn->Call();
delete fn;
static void _menu_activate_callback(GtkWidget* gtk_menu, gpointer data) {
GTKMenuItem* menu = reinterpret_cast<GTKMenuItem*>(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<void()> 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<const gchar*>(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

View File

@@ -15,6 +15,7 @@
#include <gdk/gdkx.h>
#include <gtk/gtk.h>
#include <xcb/xcb.h>
#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 {}

View File

@@ -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> Window::Create(Loop* loop, const std::string& title) {
return std::make_unique<Win32Window>(loop, title);
std::unique_ptr<Window> Window::Create(WindowedAppContext& app_context,
const std::string& title) {
return std::make_unique<Win32Window>(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<const Win32WindowedAppContext&>(app_context()).hinstance();
}
bool Win32Window::Initialize() { return OnCreate(); }
bool Win32Window::OnCreate() {
HINSTANCE hInstance = GetModuleHandle(nullptr);
HINSTANCE hInstance =
static_cast<const Win32WindowedAppContext&>(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<uint32_t>(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<const Win32WindowedAppContext&>(app_context()).hinstance(),
L"MAINICON");
SendMessageW(hwnd_, WM_SETICON, ICON_BIG,
reinterpret_cast<LPARAM>(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() {

View File

@@ -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;
};

129
src/xenia/ui/windowed_app.h Normal file
View File

@@ -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 <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "xenia/base/platform.h"
#include "xenia/ui/windowed_app_context.h"
#if XE_PLATFORM_ANDROID
#include <android/native_activity.h>
#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<std::string>& 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<std::string> 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<WindowedApp> (*GetWindowedAppCreator())(
WindowedAppContext& app_context);
#define XE_DEFINE_WINDOWED_APP(export_name, creator) \
std::unique_ptr<xe::ui::WindowedApp> (*xe::ui::GetWindowedAppCreator())( \
xe::ui::WindowedAppContext & app_context) { \
return creator; \
}
#endif
} // namespace ui
} // namespace xe
#endif // XENIA_UI_WINDOWED_APP_H_

View File

@@ -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 <utility>
#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<void()> function) {
{
std::unique_lock<std::mutex> 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<void()> 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<void()> 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<std::mutex> 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<void()> 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

View File

@@ -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 <cstdint>
#include <deque>
#include <functional>
#include <mutex>
#include <thread>
#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<void()> function);
// Executes the function immediately if already in the UI thread, enqueues it
// otherwise.
bool CallInUIThread(std::function<void()> function);
bool CallInUIThreadSynchronous(std::function<void()> 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<std::function<void()>> 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_

View File

@@ -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 <android/native_activity.h>
#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<WindowedApp> (*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<WindowedApp> (
*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

View File

@@ -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 <android/native_activity.h>
#include <memory>
#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<WindowedApp> (*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<WindowedApp> (*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<WindowedApp> 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_

View File

@@ -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 <gdk/gdk.h>
#include <glib.h>
#include <gtk/gtk.h>
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<std::mutex> 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<std::mutex> 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<GTKWindowedAppContext*>(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<std::mutex> 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<GTKWindowedAppContext*>(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

View File

@@ -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 <glib.h>
#include <mutex>
#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_

View File

@@ -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 <cstdlib>
#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<LONG_PTR>(
reinterpret_cast<const CREATESTRUCTW*>(lparam)->lpCreateParams));
} else {
auto app_context = reinterpret_cast<Win32WindowedAppContext*>(
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

View File

@@ -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 <memory>
#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_

View File

@@ -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 <gtk/gtk.h>
#include <cstdio>
#include <cstdlib>
#include <memory>
#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<xe::ui::WindowedApp> 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;
}

View File

@@ -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 <cstdlib>
#include <memory>
#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<xe::ui::WindowedApp> 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;
}