[UI] Convert to Qt

This commit is contained in:
Herman S.
2025-10-04 12:22:24 +09:00
parent 6300fc7fa7
commit 213dcc2675
22 changed files with 1539 additions and 15 deletions
+20 -3
View File
@@ -51,7 +51,7 @@ jobs:
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
sudo apt-add-repository "deb http://apt.llvm.org/${UBUNTU_BASE}/ llvm-toolchain-${UBUNTU_BASE}-20 main"
sudo apt-get -y update
sudo apt-get -y install mesa-vulkan-drivers valgrind libc++-dev libc++abi-dev libgtk-3-dev libsdl2-dev libvulkan-dev libx11-xcb-dev clang-20 ninja-build spirv-tools
sudo apt-get -y install mesa-vulkan-drivers valgrind libc++-dev libc++abi-dev libgtk-3-dev libsdl2-dev libvulkan-dev libx11-xcb-dev clang-20 ninja-build spirv-tools qt6-base-dev
# Verify spirv-opt is available
which spirv-opt && spirv-opt --version || echo "Warning: spirv-opt not found in PATH"
git submodule update --init --depth=1 -j$(getconf _NPROCESSORS_ONLN) $(grep -oP '(?<=path = )(?!third_party\/(DirectXShaderCompiler|FidelityFX-(CAS|FSR)|premake-(androidndk|export-compile-commands))).+' .gitmodules)
@@ -103,6 +103,15 @@ jobs:
restore-keys: |
${{ runner.os }}-vulkan-sdk-
- name: Cache Qt6
id: cache-qt
uses: actions/cache@v3
with:
path: C:\Qt
key: ${{ runner.os }}-qt6-6.8.1
restore-keys: |
${{ runner.os }}-qt6-
- name: Cache submodules
id: cache-submodules
uses: actions/cache@v3
@@ -133,6 +142,13 @@ jobs:
echo "Warning: spirv-opt.exe not found at expected location"
}
# Install Qt6
$qtCacheHit = '${{ steps.cache-qt.outputs.cache-hit }}'
if ($qtCacheHit -ne 'true') {
pip install aqtinstall
aqt install-qt windows desktop 6.8.1 win64_msvc2022_64 -O C:\Qt
}
$cacheHit = '${{ steps.cache-submodules.outputs.cache-hit }}'
if ($cacheHit -ne 'true') {
git submodule update --init --depth=1 -j $env:NUMBER_OF_PROCESSORS (Select-String -CaseSensitive '(?<=path = )(?!third_party\/(FidelityFX-(CAS|FSR)|premake-(androidndk|cmake|export-compile-commands))).+' .gitmodules).Matches.Value
@@ -148,8 +164,9 @@ jobs:
echo "::error::$file is too small."
}
}
robocopy . build\bin\Windows\Release LICENSE /r:0 /w:0
robocopy build\bin\Windows\Release artifacts\xenia_edge xenia_edge.exe xenia_edge.pdb LICENSE /r:0 /w:0
robocopy . build\bin\Windows\Release LICENSE /r:0 /w:0
# Copy all files and directories (Qt DLLs, plugins, etc.) excluding build artifacts
robocopy build\bin\Windows\Release artifacts\xenia_edge /MIR /r:0 /w:0 /XF *.lib *.idb *.exp /XD generic iconengines imageformats networkinformation styles tls
If ($LastExitCode -le 7) { echo "LastExitCode = $LastExitCode";$LastExitCode = 0 }
- name: Upload xenia edge artifacts
if: steps.prepare_artifacts.outcome == 'success'
+2
View File
@@ -14,6 +14,8 @@ drivers.
* Windows 11 SDK version 10.0.22000.0 (for Visual Studio 2022, this or any newer version)
* [Python 3.9+ 64-bit](https://www.python.org/downloads/)
* Ensure Python is in PATH.
* [Vulkan SDK](https://sdk.lunarg.com/sdk/download/latest/windows/vulkan-sdk.exe)
* The build script will automatically detect it if installed at `C:\VulkanSDK`
```
git clone https://github.com/has207/xenia-edge.git
+38
View File
@@ -133,6 +133,7 @@ filter("platforms:Linux")
system("linux")
toolset("clang")
pkg_config.all("gtk+-x11-3.0")
pkg_config.all("Qt6Core Qt6Gui Qt6Widgets")
links({
"stdc++fs",
"dl",
@@ -225,6 +226,8 @@ filter("platforms:Windows")
toolset("msc")
buildoptions({
"/utf-8", -- 'build correctly on systems with non-Latin codepages'.
"/Zc:__cplusplus", -- Enable correct __cplusplus macro value (required for Qt6).
"/Zc:preprocessor", -- Enable conformant preprocessor (supports #if in macro args).
-- Disable warnings
"/wd4201", -- Nameless struct/unions are ok.
})
@@ -256,6 +259,41 @@ filter("platforms:Windows")
"bcrypt",
})
local qt_dir = os.getenv("QT_DIR")
if qt_dir then
includedirs({
path.join(qt_dir, "include"),
path.join(qt_dir, "include/QtCore"),
path.join(qt_dir, "include/QtGui"),
path.join(qt_dir, "include/QtWidgets"),
})
libdirs({
path.join(qt_dir, "lib"),
})
end
filter({"platforms:Windows", "configurations:Release"})
local qt_dir = os.getenv("QT_DIR")
if qt_dir then
links({
"Qt6Core",
"Qt6Gui",
"Qt6Widgets",
})
end
filter({"platforms:Windows", "configurations:Debug or Checked"})
local qt_dir = os.getenv("QT_DIR")
if qt_dir then
links({
"Qt6Cored",
"Qt6Guid",
"Qt6Widgetsd",
})
end
filter("platforms:Windows")
-- Embed the manifest for things like dependencies and DPI awareness.
filter({"platforms:Windows", "kind:ConsoleApp or WindowedApp"})
files({
+2 -1
View File
@@ -43,7 +43,7 @@ project("xenia-app")
local_platform_files()
files({
"../base/main_init_"..platform_suffix..".cc",
"../ui/windowed_app_main_"..platform_suffix..".cc",
"../ui/windowed_app_main_qt.cc",
})
resincludedirs({
@@ -85,6 +85,7 @@ project("xenia-app")
files({
"main_resources.rc",
})
linkoptions({"/ENTRY:mainCRTStartup"})
filter({"architecture:x86_64", "files:../base/main_init_"..platform_suffix..".cc"})
vectorextensions("SSE2") -- Disable AVX for main_init_win.cc so our AVX check doesn't use AVX instructions.
+5 -3
View File
@@ -40,9 +40,11 @@ std::filesystem::path to_path(const std::u16string_view source) {
namespace filesystem {
std::filesystem::path GetExecutablePath() {
wchar_t* path;
auto error = _get_wpgmptr(&path);
return !error ? std::filesystem::path(path) : std::filesystem::path();
// Don't use _get_wpgmptr in Qt apps - CRT may not be initialized properly
// Use GetModuleFileName directly instead
wchar_t buffer[MAX_PATH];
DWORD len = GetModuleFileNameW(nullptr, buffer, MAX_PATH);
return len > 0 ? std::filesystem::path(buffer) : std::filesystem::path();
}
std::filesystem::path GetExecutableFolder() {
+7
View File
@@ -51,6 +51,13 @@ project("xenia-gpu-shader-compiler")
"../base/console_app_main_"..platform_suffix..".cc",
})
-- Include SPIRV-Tools headers from Vulkan SDK
filter("platforms:Windows")
includedirs({
"$(VULKAN_SDK)/Include",
})
filter({})
filter("platforms:Windows")
-- Only create the .user file if it doesn't already exist.
local user_file = project_root.."/build/xenia-gpu-shader-compiler.vcxproj.user"
+6
View File
@@ -1068,6 +1068,12 @@ Presenter::PaintResult D3D12Presenter::PaintAndPresentImpl(
0, DXGI_PRESENT_RESTART | (paint_context_.swap_chain_allows_tearing
? DXGI_PRESENT_ALLOW_TEARING
: 0));
static bool logged_present = false;
if (!logged_present) {
XELOGI("D3D12Presenter: First Present() call, result: 0x{:08X}",
present_result);
logged_present = true;
}
// Even if presentation has failed, work might have been enqueued anyway
// internally before the failure according to Jesse Natalie from the DirectX
// Discord server.
+105
View File
@@ -0,0 +1,105 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/ui/file_picker.h"
#include <string>
#include <QFileDialog>
#include <QString>
#include "xenia/base/assert.h"
#include "xenia/base/filesystem.h"
#include "xenia/base/logging.h"
#include "xenia/base/string.h"
#include "xenia/ui/window_qt.h"
namespace xe {
namespace ui {
class QtFilePicker : public FilePicker {
public:
QtFilePicker();
~QtFilePicker() override;
bool Show(Window* parent_window) override;
private:
};
std::unique_ptr<FilePicker> FilePicker::Create() {
return std::make_unique<QtFilePicker>();
}
QtFilePicker::QtFilePicker() = default;
QtFilePicker::~QtFilePicker() = default;
bool QtFilePicker::Show(Window* parent_window) {
QString title;
QFileDialog::FileMode file_mode;
QFileDialog::AcceptMode accept_mode;
switch (mode()) {
case Mode::kOpen:
if (type() == Type::kFile) {
title = "Open File";
file_mode = QFileDialog::ExistingFile;
} else {
title = "Open Directory";
file_mode = QFileDialog::Directory;
}
accept_mode = QFileDialog::AcceptOpen;
break;
case Mode::kSave:
title = "Save File";
file_mode = QFileDialog::AnyFile;
accept_mode = QFileDialog::AcceptSave;
break;
default:
XELOGE("QtFilePicker::Show: Unhandled mode: {}, Type: {}",
static_cast<int>(mode()), static_cast<int>(type()));
assert_always();
return false;
}
auto* qt_window =
parent_window ? dynamic_cast<QtWindow*>(parent_window) : nullptr;
// Use static QFileDialog function to avoid Qt container ABI issues on Windows
QString initial_dir;
if (!initial_directory().empty()) {
std::string dir_str = initial_directory().string();
initial_dir =
QString::fromUtf8(dir_str.c_str(), static_cast<int>(dir_str.size()));
}
QString file_path = QFileDialog::getOpenFileName(
qt_window ? qt_window->qwindow() : nullptr, title, initial_dir,
QString(), // filter
nullptr, // selected filter
QFileDialog::DontUseNativeDialog // Use Qt dialog to avoid native API
// issues
);
if (!file_path.isEmpty()) {
QByteArray utf8_bytes = file_path.toUtf8();
std::string file_path_str(utf8_bytes.constData(), utf8_bytes.size());
std::vector<std::filesystem::path> selected_files;
selected_files.push_back(xe::to_path(file_path_str));
set_selected_files(selected_files);
return true;
}
return false;
}
} // namespace ui
} // namespace xe
+3
View File
@@ -830,6 +830,9 @@ void ImGuiDrawer::OnKey(KeyEvent& e, bool is_down) {
default:
break;
}
if (presenter_) {
presenter_->RequestUIPaintFromUIThread();
}
}
void ImGuiDrawer::UpdateMousePosition(float x, float y) {
+1 -1
View File
@@ -60,7 +60,7 @@ void MenuItem::RemoveChild(MenuItem* child_item) {
}
}
MenuItem* MenuItem::child(size_t index) { return children_[index].get(); }
MenuItem* MenuItem::child(size_t index) const { return children_[index].get(); }
void MenuItem::OnSelected() {
if (callback_) {
+2 -1
View File
@@ -57,7 +57,8 @@ class MenuItem {
void AddChild(std::unique_ptr<MenuItem> child_item);
void AddChild(MenuItemPtr child_item);
void RemoveChild(MenuItem* child_item);
MenuItem* child(size_t index);
MenuItem* child(size_t index) const;
size_t child_count() const { return children_.size(); }
virtual void SetEnabled(bool enabled) {}
View File
+9 -1
View File
@@ -12,7 +12,15 @@ project("xenia-ui")
local_platform_files()
removefiles({
"*_demo.cc",
"windowed_app_main_*.cc",
-- Exclude old platform-specific implementations in favor of Qt
"window_gtk.cc",
"window_win.cc",
"file_picker_gtk.cc",
"file_picker_win.cc",
"windowed_app_context_gtk.cc",
"windowed_app_context_win.cc",
"windowed_app_main_posix.cc",
"windowed_app_main_win.cc",
})
if os.istarget("android") then
filter("platforms:Android-*")
+21 -1
View File
@@ -16,6 +16,7 @@
#include "xenia/ui/window.h"
#if XE_PLATFORM_WIN32
#include "xenia/ui/surface_win.h"
#include "xenia/ui/window_win.h"
#endif
@@ -1208,7 +1209,12 @@ void Presenter::UpdateSurfaceMonitorFromUIThread(
#if XE_PLATFORM_WIN32
HMONITOR surface_new_win32_monitor = nullptr;
if (surface_) {
HWND hwnd = static_cast<const Win32Window*>(window_)->hwnd();
// Get HWND from the surface instead of assuming Win32Window
// This supports both Win32Window and QtWindow
HWND hwnd = nullptr;
if (surface_->GetType() == Surface::kTypeIndex_Win32Hwnd) {
hwnd = static_cast<const Win32HwndSurface*>(surface_)->hwnd();
}
// The HWND may be non-existent if the window has been closed and destroyed
// (the HWND, not the xe::ui::Window) already.
if (hwnd) {
@@ -1382,6 +1388,20 @@ void Presenter::WaitForUITickFromUIThread() {
}
dxgi_ui_tick_signal_condition_.wait(dxgi_ui_tick_lock);
}
#elif XE_PLATFORM_LINUX
if (!AreUITicksNeededFromUIThread()) {
return;
}
// On Linux, implement timer-based rate limiting at 60 FPS to match game frame
// rate.
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(
now - linux_ui_tick_last_paint_time_);
constexpr auto frame_time = std::chrono::microseconds(16667); // ~60 FPS
if (elapsed < frame_time) {
std::this_thread::sleep_for(frame_time - elapsed);
}
linux_ui_tick_last_paint_time_ = std::chrono::steady_clock::now();
#endif // XE_PLATFORM
}
+5
View File
@@ -13,6 +13,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <climits>
#include <cmath>
#include <condition_variable>
@@ -956,6 +957,10 @@ class Presenter {
std::array<GuestOutputProperties, kGuestOutputMailboxSize>
guest_output_properties_;
#if XE_PLATFORM_LINUX
std::chrono::steady_clock::time_point linux_ui_tick_last_paint_time_;
#endif
// Accessible only by refreshing, whether the last refresh contained an image
// rather than being blank.
bool guest_output_active_last_refresh_ = false;
+2 -2
View File
@@ -484,8 +484,6 @@ VirtualKey GTKWindow::TranslateVirtualKey(guint keyval) {
return VirtualKey::kSpace;
case GDK_KEY_Caps_Lock:
return VirtualKey::kCapital;
case GDK_KEY_Escape:
return VirtualKey::kEscape;
case GDK_KEY_F1:
return VirtualKey::kF1;
case GDK_KEY_F2:
@@ -521,6 +519,8 @@ VirtualKey GTKWindow::TranslateVirtualKey(guint keyval) {
case GDK_KEY_Pause:
case GDK_KEY_Break:
return VirtualKey::kPause;
case GDK_KEY_Escape:
return VirtualKey::kEscape;
default:
XELOGW("Unhandled key code: {}", keyval);
return VirtualKey(keyval);
File diff suppressed because it is too large Load Diff
+113
View File
@@ -0,0 +1,113 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_UI_WINDOW_QT_H_
#define XENIA_UI_WINDOW_QT_H_
#include <memory>
#include <string>
#include <QMainWindow>
#include <QWidget>
#include "xenia/ui/menu_item.h"
#include "xenia/ui/window.h"
namespace xe {
namespace ui {
class QtWindowedAppContext;
class QtWindowInternal;
class QtWindow : public Window {
using super = Window;
public:
QtWindow(WindowedAppContext& app_context, const std::string_view title,
uint32_t desired_logical_width, uint32_t desired_logical_height);
~QtWindow() override;
QMainWindow* qwindow() const { return qwindow_; }
void TriggerPaint() { OnPaint(); }
// Public accessors for event handling from ExternalRenderWidget
void OnMouseMoveEvent(QMouseEvent* event);
void OnMousePressEvent(QMouseEvent* event);
void OnMouseReleaseEvent(QMouseEvent* event);
void OnWheelEvent(QWheelEvent* event);
protected:
bool OpenImpl() override;
void RequestCloseImpl() override;
void ApplyNewFullscreen() override;
void ApplyNewTitle() override;
void ApplyNewMainMenu(MenuItem* old_main_menu) override;
void ApplyNewCursorVisibility(
CursorVisibility old_cursor_visibility) override;
void FocusImpl() override;
std::unique_ptr<Surface> CreateSurfaceImpl(
Surface::TypeFlags allowed_types) override;
void RequestPaintImpl() override;
private:
friend class QtWindowInternal;
friend class ExternalRenderWidget;
void HandleSizeUpdate(WindowDestructionReceiver& destruction_receiver);
static VirtualKey TranslateVirtualKey(int qt_key);
void OnCloseEvent(QCloseEvent* event);
void OnResizeEvent(QResizeEvent* event);
void OnKeyPressEvent(QKeyEvent* event);
void OnKeyReleaseEvent(QKeyEvent* event);
bool OnEventFilter(QObject* obj, QEvent* event);
void SetCursorAutoHideTimer();
void OnCursorAutoHideTimeout();
void UpdateCursorVisibility();
QMainWindow* qwindow_ = nullptr;
QWidget* drawing_widget_ = nullptr;
bool in_size_update_ = false;
// Cursor auto-hide support
QTimer* cursor_auto_hide_timer_ = nullptr;
QPoint cursor_auto_hide_last_pos_;
bool cursor_currently_auto_hidden_ = false;
};
class QtMenuItem : public MenuItem {
public:
QtMenuItem(Type type, const std::string& text, const std::string& hotkey,
std::function<void()> callback);
~QtMenuItem() override;
QAction* action() const { return action_; }
QMenu* menu() const { return menu_; }
protected:
void OnChildAdded(MenuItem* child_item) override;
void OnChildRemoved(MenuItem* child_item) override;
private:
void OnTriggered();
QAction* action_ = nullptr;
QMenu* menu_ = nullptr;
};
} // namespace ui
} // namespace xe
#endif // XENIA_UI_WINDOW_QT_H_
+74
View File
@@ -0,0 +1,74 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/ui/windowed_app_context_qt.h"
#include <QApplication>
#include <QMetaObject>
#include <QTimer>
#if XE_PLATFORM_WIN32
#include "xenia/base/platform_win.h"
#endif
namespace xe {
namespace ui {
QtWindowedAppContext::QtWindowedAppContext(QApplication* app)
: app_(app)
#if XE_PLATFORM_WIN32
,
hinstance_(GetModuleHandle(nullptr))
#endif
{
pending_functions_timer_ = new QTimer();
pending_functions_timer_->setSingleShot(true);
QObject::connect(pending_functions_timer_, &QTimer::timeout,
[this]() { ExecutePendingFunctionsFromTimer(); });
}
QtWindowedAppContext::~QtWindowedAppContext() {
if (pending_functions_timer_) {
pending_functions_timer_->stop();
delete pending_functions_timer_;
pending_functions_timer_ = nullptr;
}
}
void QtWindowedAppContext::NotifyUILoopOfPendingFunctions() {
// Use QMetaObject::invokeMethod on the timer to safely call from any thread
QMetaObject::invokeMethod(
pending_functions_timer_, [this]() { StartTimerInternal(); },
Qt::QueuedConnection);
}
void QtWindowedAppContext::StartTimerInternal() {
if (!pending_functions_timer_->isActive()) {
pending_functions_timer_->start(0);
}
}
void QtWindowedAppContext::PlatformQuitFromUIThread() {
if (app_) {
app_->quit();
}
}
void QtWindowedAppContext::RunMainQtLoop() {
if (app_) {
app_->exec();
}
}
void QtWindowedAppContext::ExecutePendingFunctionsFromTimer() {
ExecutePendingFunctionsFromUIThread();
}
} // namespace ui
} // namespace xe
+58
View File
@@ -0,0 +1,58 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_UI_WINDOWED_APP_CONTEXT_QT_H_
#define XENIA_UI_WINDOWED_APP_CONTEXT_QT_H_
#include <mutex>
#include "xenia/base/platform.h"
#include "xenia/ui/windowed_app_context.h"
#if XE_PLATFORM_WIN32
#include "xenia/base/platform_win.h"
#endif
class QApplication;
class QTimer;
namespace xe {
namespace ui {
class QtWindowedAppContext final : public WindowedAppContext {
public:
explicit QtWindowedAppContext(QApplication* app);
~QtWindowedAppContext();
#if XE_PLATFORM_WIN32
HINSTANCE hinstance() const { return hinstance_; }
#endif
void NotifyUILoopOfPendingFunctions() override;
void PlatformQuitFromUIThread() override;
void RunMainQtLoop();
private:
void ExecutePendingFunctionsFromTimer();
void StartTimerInternal();
QApplication* app_;
QTimer* pending_functions_timer_ = nullptr;
#if XE_PLATFORM_WIN32
HINSTANCE hinstance_;
#endif
};
} // namespace ui
} // namespace xe
#endif // XENIA_UI_WINDOWED_APP_CONTEXT_QT_H_

Some files were not shown because too many files have changed in this diff Show More