Update 'qt-experimental' to match 'master'

This commit is contained in:
Jonathan Goyvaerts 2020-08-23 04:27:14 +02:00 committed by Satori
parent dffdf92e39
commit 3459ee11d4
185 changed files with 16296 additions and 1168 deletions

View File

@ -28,6 +28,15 @@ os: Visual Studio 2019
init:
- git config --global core.autocrlf input
# build cache
cache:
- C:\Qt\5.11.2\msvc2017_64 -> .appveyor.yml
# environment variables
environment:
QT_DIR: C:\Qt\5.11.2\msvc2017_64
QT_STATIC: 1
install:
- cmd: xb setup

3
.gitmodules vendored
View File

@ -64,3 +64,6 @@
[submodule "third_party/DirectXShaderCompiler"]
path = third_party/DirectXShaderCompiler
url = https://github.com/microsoft/DirectXShaderCompiler.git
[submodule "third_party/premake-qt"]
path = third_party/premake-qt
url = https://github.com/dcourtois/premake-qt.git

View File

@ -36,10 +36,16 @@ git:
# We handle submodules ourselves in xenia-build setup.
submodules: false
before_install:
# Qt
- sudo add-apt-repository -y ppa:ubuntu-sdk-team/ppa
- sudo apt-get update -yq
- sudo apt-get install qt5-default
before_script:
- export LIBVULKAN_VERSION=1.1.70
- export CXX=$CXX_COMPILER
- export CC=$C_COMPILER
- export QT_DIR=/usr
# Dump useful info.
- $CXX --version
- python3 --version

View File

@ -1,5 +1,6 @@
include("tools/build")
require("third_party/premake-export-compile-commands/export-compile-commands")
require("third_party/premake-qt/qt")
location(build_root)
targetdir(build_bin)
@ -248,6 +249,7 @@ solution("xenia")
include("src/xenia/hid/sdl")
include("src/xenia/kernel")
include("src/xenia/ui")
include("src/xenia/ui/qt")
include("src/xenia/ui/spirv")
include("src/xenia/ui/vulkan")
include("src/xenia/vfs")

View File

@ -2,467 +2,204 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Copyright 2018 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/app/emulator_window.h"
#include "third_party/fmt/include/fmt/format.h"
#include <QVulkanWindow>
#include "third_party/imgui/imgui.h"
#include "xenia/apu/xaudio2/xaudio2_audio_system.h"
#include "xenia/base/clock.h"
#include "xenia/base/cvar.h"
#include "xenia/base/debugging.h"
#include "xenia/base/logging.h"
#include "xenia/base/platform.h"
#include "xenia/base/profiling.h"
#include "xenia/base/system.h"
#include "xenia/base/threading.h"
#include "xenia/emulator.h"
#include "xenia/gpu/graphics_system.h"
#include "xenia/ui/file_picker.h"
#include "xenia/ui/imgui_dialog.h"
#include "xenia/ui/imgui_drawer.h"
#include "xenia/gpu/vulkan/vulkan_graphics_system.h"
#include "xenia/hid/input_system.h"
#include "xenia/hid/xinput/xinput_hid.h"
#include "xenia/ui/vulkan/vulkan_instance.h"
#include "xenia/ui/vulkan/vulkan_provider.h"
// Autogenerated by `xb premake`.
#include "build/version.h"
DEFINE_string(apu, "any", "Audio system. Use: [any, nop, xaudio2]", "General");
DEFINE_string(gpu, "any", "Graphics system. Use: [any, vulkan, null]",
"General");
DEFINE_string(hid, "any", "Input system. Use: [any, nop, winkey, xinput]",
"General");
DECLARE_bool(debug);
DEFINE_string(target, "", "Specifies the target .xex or .iso to execute.",
"General");
DEFINE_bool(fullscreen, false, "Toggles fullscreen", "General");
namespace xe {
namespace app {
using xe::ui::FileDropEvent;
using xe::ui::KeyEvent;
using xe::ui::MenuItem;
using xe::ui::MouseEvent;
using xe::ui::UIEvent;
class VulkanWindow : public QVulkanWindow {
public:
VulkanWindow(gpu::vulkan::VulkanGraphicsSystem* gfx)
: graphics_system_(gfx) {}
QVulkanWindowRenderer* createRenderer() override;
const std::string kBaseTitle = "xenia";
private:
gpu::vulkan::VulkanGraphicsSystem* graphics_system_;
};
EmulatorWindow::EmulatorWindow(Emulator* emulator)
: emulator_(emulator),
loop_(ui::Loop::Create()),
window_(ui::Window::Create(loop_.get(), kBaseTitle)) {
base_title_ = kBaseTitle +
#ifdef DEBUG
#if _NO_DEBUG_HEAP == 1
" DEBUG"
#else
" CHECKED"
#endif
#endif
" (" XE_BUILD_BRANCH "/" XE_BUILD_COMMIT_SHORT "/" XE_BUILD_DATE
")";
}
class VulkanRenderer : public QVulkanWindowRenderer {
public:
VulkanRenderer(VulkanWindow* window,
gpu::vulkan::VulkanGraphicsSystem* graphics_system)
: window_(window), graphics_system_(graphics_system) {}
EmulatorWindow::~EmulatorWindow() {
loop_->PostSynchronous([this]() { window_.reset(); });
}
void startNextFrame() override {
// Copy the graphics frontbuffer to our backbuffer.
//auto swap_state = graphics_system_->swap_state();
std::unique_ptr<EmulatorWindow> EmulatorWindow::Create(Emulator* emulator) {
std::unique_ptr<EmulatorWindow> emulator_window(new EmulatorWindow(emulator));
auto cmd = window_->currentCommandBuffer();
//auto src = reinterpret_cast<VkImage>(
// swap_state->buffer_textures[swap_state->current_buffer]);
auto dest = window_->swapChainImage(window_->currentSwapChainImageIndex());
auto dest_size = window_->swapChainImageSize();
emulator_window->loop()->PostSynchronous([&emulator_window]() {
xe::threading::set_name("Win32 Loop");
xe::Profiler::ThreadEnter("Win32 Loop");
VkImageMemoryBarrier barrier;
std::memset(&barrier, 0, sizeof(VkImageMemoryBarrier));
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
//barrier.image = src;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0,
nullptr, 1, &barrier);
if (!emulator_window->Initialize()) {
xe::FatalError("Failed to initialize main window");
return;
VkImageBlit region;
region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
region.srcOffsets[0] = {0, 0, 0};
/*region.srcOffsets[1] = {static_cast<int32_t>(swap_state->width),
static_cast<int32_t>(swap_state->height), 1};*/
region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
region.dstOffsets[0] = {0, 0, 0};
region.dstOffsets[1] = {static_cast<int32_t>(dest_size.width()),
static_cast<int32_t>(dest_size.height()), 1};
/* vkCmdBlitImage(cmd, src, VK_IMAGE_LAYOUT_GENERAL, dest,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region,
VK_FILTER_LINEAR);*/
//swap_state->pending = false;
window_->frameReady();
}
});
return emulator_window;
private:
gpu::vulkan::VulkanGraphicsSystem* graphics_system_;
VulkanWindow* window_;
};
QVulkanWindowRenderer* VulkanWindow::createRenderer() {
return new VulkanRenderer(this, graphics_system_);
}
bool EmulatorWindow::Initialize() {
if (!window_->Initialize()) {
XELOGE("Failed to initialize platform window");
EmulatorWindow::EmulatorWindow(Loop* loop, const std::string& title)
: QtWindow(loop, title) {
// TODO(DrChat): Pass in command line arguments.
emulator_ = std::make_unique<xe::Emulator>("","","");
auto audio_factory = [&](cpu::Processor* processor,
kernel::KernelState* kernel_state) {
auto audio = apu::xaudio2::XAudio2AudioSystem::Create(processor);
if (audio->Setup(kernel_state) != X_STATUS_SUCCESS) {
audio->Shutdown();
return std::unique_ptr<apu::AudioSystem>(nullptr);
}
return audio;
};
graphics_provider_ = ui::vulkan::VulkanProvider::Create(nullptr);
auto graphics_factory = [&](cpu::Processor* processor,
kernel::KernelState* kernel_state) {
auto graphics = std::make_unique<gpu::vulkan::VulkanGraphicsSystem>();
if (graphics->Setup(processor, kernel_state,
graphics_provider_->CreateOffscreenContext()->target_window())) {
graphics->Shutdown();
return std::unique_ptr<gpu::vulkan::VulkanGraphicsSystem>(nullptr);
}
return graphics;
};
auto input_factory = [&](ui::Window* window) {
std::vector<std::unique_ptr<hid::InputDriver>> drivers;
auto xinput_driver = hid::xinput::Create(window);
xinput_driver->Setup();
drivers.push_back(std::move(xinput_driver));
return drivers;
};
//X_STATUS result = emulator_->Setup(this, audio_factory, graphics_factory, input_factory);
//if (result == X_STATUS_SUCCESS) {
// // Setup a callback called when the emulator wants to swap.
// emulator_->graphics_system()->SetSwapCallback([&]() {
// QMetaObject::invokeMethod(this->graphics_window_.get(), "requestUpdate",
// Qt::QueuedConnection);
// });
//}
//// Initialize our backend display window.
//if (!InitializeVulkan()) {
// return;
//}
//// Set a callback on launch
//emulator_->on_launch.AddListener([this]() {
// auto title_db = this->emulator()->game_data();
// if (title_db) {
// QPixmap p;
// auto icon_block = title_db->icon();
// if (icon_block.buffer &&
// p.loadFromData(icon_block.buffer, uint(icon_block.size), "PNG")) {
// this->setWindowIcon(QIcon(p));
// }
// }
//});
}
bool EmulatorWindow::InitializeVulkan() {
auto provider =
reinterpret_cast<ui::vulkan::VulkanProvider*>(graphics_provider_.get());
// Create a Qt wrapper around our vulkan instance.
vulkan_instance_ = std::make_unique<QVulkanInstance>();
vulkan_instance_->setVkInstance(*provider->instance());
if (!vulkan_instance_->create()) {
return false;
}
UpdateTitle();
graphics_window_ = std::make_unique<VulkanWindow>(
reinterpret_cast<gpu::vulkan::VulkanGraphicsSystem*>(
emulator_->graphics_system()));
graphics_window_->setVulkanInstance(vulkan_instance_.get());
window_->on_closed.AddListener([this](UIEvent* e) { loop_->Quit(); });
loop_->on_quit.AddListener([this](UIEvent* e) { window_.reset(); });
window_->on_file_drop.AddListener(
[this](FileDropEvent* e) { FileDrop(e->filename()); });
window_->on_key_down.AddListener([this](KeyEvent* e) {
bool handled = true;
switch (e->key_code()) {
case 0x4F: { // o
if (e->is_ctrl_pressed()) {
FileOpen();
}
} break;
case 0x6A: { // numpad *
CpuTimeScalarReset();
} break;
case 0x6D: { // numpad minus
CpuTimeScalarSetHalf();
} break;
case 0x6B: { // numpad plus
CpuTimeScalarSetDouble();
} break;
case 0x72: { // F3
Profiler::ToggleDisplay();
} break;
case 0x73: { // VK_F4
GpuTraceFrame();
} break;
case 0x74: { // VK_F5
GpuClearCaches();
} break;
case 0x76: { // VK_F7
// Save to file
// TODO: Choose path based on user input, or from options
// TODO: Spawn a new thread to do this.
emulator()->SaveToFile("test.sav");
} break;
case 0x77: { // VK_F8
// Restore from file
// TODO: Choose path from user
// TODO: Spawn a new thread to do this.
emulator()->RestoreFromFile("test.sav");
} break;
case 0x7A: { // VK_F11
ToggleFullscreen();
} break;
case 0x1B: { // VK_ESCAPE
// Allow users to escape fullscreen (but not enter it).
if (window_->is_fullscreen()) {
window_->ToggleFullscreen(false);
} else {
handled = false;
}
} break;
case 0x13: { // VK_PAUSE
CpuBreakIntoDebugger();
} break;
case 0x03: { // VK_CANCEL
CpuBreakIntoHostDebugger();
} break;
case 0x70: { // VK_F1
ShowHelpWebsite();
} break;
case 0x71: { // VK_F2
ShowCommitID();
} break;
default: {
handled = false;
} break;
}
e->set_handled(handled);
});
window_->on_mouse_move.AddListener([this](MouseEvent* e) {
if (window_->is_fullscreen() && (e->dx() > 2 || e->dy() > 2)) {
if (!window_->is_cursor_visible()) {
window_->set_cursor_visible(true);
}
cursor_hide_time_ = Clock::QueryHostSystemTime() + 30000000;
}
e->set_handled(false);
});
window_->on_paint.AddListener([this](UIEvent* e) { CheckHideCursor(); });
// Main menu.
// FIXME: This code is really messy.
auto main_menu = MenuItem::Create(MenuItem::Type::kNormal);
auto file_menu = MenuItem::Create(MenuItem::Type::kPopup, "&File");
{
file_menu->AddChild(
MenuItem::Create(MenuItem::Type::kString, "&Open...", "Ctrl+O",
std::bind(&EmulatorWindow::FileOpen, this)));
file_menu->AddChild(
MenuItem::Create(MenuItem::Type::kString, "Close",
std::bind(&EmulatorWindow::FileClose, this)));
file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
file_menu->AddChild(MenuItem::Create(
MenuItem::Type::kString, "Show content directory...",
std::bind(&EmulatorWindow::ShowContentDirectory, this)));
file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
file_menu->AddChild(MenuItem::Create(MenuItem::Type::kString, "E&xit",
"Alt+F4",
[this]() { window_->Close(); }));
}
main_menu->AddChild(std::move(file_menu));
// CPU menu.
auto cpu_menu = MenuItem::Create(MenuItem::Type::kPopup, "&CPU");
{
cpu_menu->AddChild(MenuItem::Create(
MenuItem::Type::kString, "&Reset Time Scalar", "Numpad *",
std::bind(&EmulatorWindow::CpuTimeScalarReset, this)));
cpu_menu->AddChild(MenuItem::Create(
MenuItem::Type::kString, "Time Scalar /= 2", "Numpad -",
std::bind(&EmulatorWindow::CpuTimeScalarSetHalf, this)));
cpu_menu->AddChild(MenuItem::Create(
MenuItem::Type::kString, "Time Scalar *= 2", "Numpad +",
std::bind(&EmulatorWindow::CpuTimeScalarSetDouble, this)));
}
cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
{
cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kString,
"Toggle Profiler &Display", "F3",
[]() { Profiler::ToggleDisplay(); }));
cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kString,
"&Pause/Resume Profiler", "`",
[]() { Profiler::TogglePause(); }));
}
cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
{
cpu_menu->AddChild(MenuItem::Create(
MenuItem::Type::kString, "&Break and Show Guest Debugger",
"Pause/Break", std::bind(&EmulatorWindow::CpuBreakIntoDebugger, this)));
cpu_menu->AddChild(MenuItem::Create(
MenuItem::Type::kString, "&Break into Host Debugger",
"Ctrl+Pause/Break",
std::bind(&EmulatorWindow::CpuBreakIntoHostDebugger, this)));
}
main_menu->AddChild(std::move(cpu_menu));
// GPU menu.
auto gpu_menu = MenuItem::Create(MenuItem::Type::kPopup, "&GPU");
{
gpu_menu->AddChild(
MenuItem::Create(MenuItem::Type::kString, "&Trace Frame", "F4",
std::bind(&EmulatorWindow::GpuTraceFrame, this)));
}
gpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
{
gpu_menu->AddChild(
MenuItem::Create(MenuItem::Type::kString, "&Clear Runtime Caches", "F5",
std::bind(&EmulatorWindow::GpuClearCaches, this)));
}
main_menu->AddChild(std::move(gpu_menu));
// Window menu.
auto window_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Window");
{
window_menu->AddChild(
MenuItem::Create(MenuItem::Type::kString, "&Fullscreen", "F11",
std::bind(&EmulatorWindow::ToggleFullscreen, this)));
}
main_menu->AddChild(std::move(window_menu));
// Help menu.
auto help_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Help");
{
help_menu->AddChild(
MenuItem::Create(MenuItem::Type::kString, "Build commit on GitHub...",
"F2", std::bind(&EmulatorWindow::ShowCommitID, this)));
help_menu->AddChild(MenuItem::Create(
MenuItem::Type::kString, "Recent changes on GitHub...", [this]() {
LaunchWebBrowser(
"https://github.com/xenia-project/xenia/compare/" XE_BUILD_COMMIT
"..." XE_BUILD_BRANCH);
}));
help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
help_menu->AddChild(
MenuItem::Create(MenuItem::Type::kString, "&Website...", "F1",
std::bind(&EmulatorWindow::ShowHelpWebsite, this)));
help_menu->AddChild(MenuItem::Create(
MenuItem::Type::kString, "&About...",
[this]() { LaunchWebBrowser("https://xenia.jp/about/"); }));
}
main_menu->AddChild(std::move(help_menu));
window_->set_main_menu(std::move(main_menu));
window_->Resize(1280, 720);
window_->DisableMainMenu();
// Now set the graphics window as our central widget.
QWidget* wrapper = QWidget::createWindowContainer(graphics_window_.get());
setCentralWidget(wrapper);
return true;
}
void EmulatorWindow::FileDrop(const std::filesystem::path& filename) {
auto result = emulator_->LaunchPath(filename);
if (XFAILED(result)) {
// TODO: Display a message box.
XELOGE("Failed to launch target: {:08X}", result);
}
}
void EmulatorWindow::FileOpen() {
std::filesystem::path path;
auto file_picker = xe::ui::FilePicker::Create();
file_picker->set_mode(ui::FilePicker::Mode::kOpen);
file_picker->set_type(ui::FilePicker::Type::kFile);
file_picker->set_multi_selection(false);
file_picker->set_title("Select Content Package");
file_picker->set_extensions({
{"Supported Files", "*.iso;*.xex;*.xcp;*.*"},
{"Disc Image (*.iso)", "*.iso"},
{"Xbox Executable (*.xex)", "*.xex"},
//{"Content Package (*.xcp)", "*.xcp" },
{"All Files (*.*)", "*.*"},
});
if (file_picker->Show(window_->native_handle())) {
auto selected_files = file_picker->selected_files();
if (!selected_files.empty()) {
path = selected_files[0];
}
}
if (!path.empty()) {
// Normalize the path and make absolute.
auto abs_path = std::filesystem::absolute(path);
auto result = emulator_->LaunchPath(abs_path);
if (XFAILED(result)) {
// TODO: Display a message box.
XELOGE("Failed to launch target: {:08X}", result);
}
}
}
void EmulatorWindow::FileClose() {
if (emulator_->is_title_open()) {
emulator_->TerminateTitle();
}
}
void EmulatorWindow::ShowContentDirectory() {
std::filesystem::path target_path;
auto content_root = emulator_->content_root();
if (!emulator_->is_title_open() || !emulator_->kernel_state()) {
target_path = content_root;
} else {
// TODO(gibbed): expose this via ContentManager?
auto title_id =
fmt::format("{:08X}", emulator_->kernel_state()->title_id());
auto package_root = content_root / title_id;
target_path = package_root;
}
if (!std::filesystem::exists(target_path)) {
std::filesystem::create_directories(target_path);
}
LaunchFileExplorer(target_path);
}
void EmulatorWindow::CheckHideCursor() {
if (!window_->is_fullscreen()) {
// Only hide when fullscreen.
return;
}
if (Clock::QueryHostSystemTime() > cursor_hide_time_) {
window_->set_cursor_visible(false);
}
}
void EmulatorWindow::CpuTimeScalarReset() {
Clock::set_guest_time_scalar(1.0);
UpdateTitle();
}
void EmulatorWindow::CpuTimeScalarSetHalf() {
Clock::set_guest_time_scalar(Clock::guest_time_scalar() / 2.0);
UpdateTitle();
}
void EmulatorWindow::CpuTimeScalarSetDouble() {
Clock::set_guest_time_scalar(Clock::guest_time_scalar() * 2.0);
UpdateTitle();
}
void EmulatorWindow::CpuBreakIntoDebugger() {
if (!cvars::debug) {
xe::ui::ImGuiDialog::ShowMessageBox(window_.get(), "Xenia Debugger",
"Xenia must be launched with the "
"--debug flag in order to enable "
"debugging.");
return;
}
auto processor = emulator()->processor();
if (processor->execution_state() == cpu::ExecutionState::kRunning) {
// Currently running, so interrupt (and show the debugger).
processor->Pause();
} else {
// Not running, so just bring the debugger into focus.
processor->ShowDebugger();
}
}
void EmulatorWindow::CpuBreakIntoHostDebugger() { xe::debugging::Break(); }
void EmulatorWindow::GpuTraceFrame() {
emulator()->graphics_system()->RequestFrameTrace();
}
void EmulatorWindow::GpuClearCaches() {
emulator()->graphics_system()->ClearCaches();
}
void EmulatorWindow::ToggleFullscreen() {
window_->ToggleFullscreen(!window_->is_fullscreen());
// Hide the cursor after a second if we're going fullscreen
cursor_hide_time_ = Clock::QueryHostSystemTime() + 30000000;
if (!window_->is_fullscreen()) {
window_->set_cursor_visible(true);
}
}
void EmulatorWindow::ShowHelpWebsite() { LaunchWebBrowser("https://xenia.jp"); }
void EmulatorWindow::ShowCommitID() {
LaunchWebBrowser(
"https://github.com/xenia-project/xenia/commit/" XE_BUILD_COMMIT "/");
}
void EmulatorWindow::UpdateTitle() {
std::string title(base_title_);
if (emulator()->is_title_open()) {
auto game_title = emulator()->game_title();
title += fmt::format(" | [{:08X}] {}", emulator()->title_id(), game_title);
}
auto graphics_system = emulator()->graphics_system();
if (graphics_system) {
auto graphics_name = graphics_system->name();
title += fmt::format(" <{}>", graphics_name);
}
if (Clock::guest_time_scalar() != 1.0) {
title += fmt::format(" (@{:.2f}x)", Clock::guest_time_scalar());
}
if (initializing_shader_storage_) {
title +=
" (Preloading shaders"
u8"\u2026"
")";
}
window_->set_title(title);
}
void EmulatorWindow::SetInitializingShaderStorage(bool initializing) {
if (initializing_shader_storage_ == initializing) {
return;
}
initializing_shader_storage_ = initializing;
UpdateTitle();
bool EmulatorWindow::Launch(const std::string& path) {
return emulator_->LaunchPath(path) == X_STATUS_SUCCESS;
}
} // namespace app

View File

@ -2,72 +2,63 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Copyright 2018 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_APP_EMULATOR_WINDOW_H_
#define XENIA_APP_EMULATOR_WINDOW_H_
#ifndef XENIA_APP_MAIN_WINDOW_H_
#define XENIA_APP_MAIN_WINDOW_H_
#include <memory>
#include <string>
#include <QMainWindow>
#include <QVulkanInstance>
#include <QWindow>
#include "xenia/ui/loop.h"
#include "xenia/ui/menu_item.h"
#include "xenia/ui/window.h"
#include "xenia/xbox.h"
namespace xe {
class Emulator;
} // namespace xe
#include "xenia/emulator.h"
#include "xenia/ui/graphics_context.h"
#include "xenia/ui/graphics_provider.h"
#include "xenia/ui/qt/window_qt.h"
#include "xenia/ui/qt/loop_qt.h"
namespace xe {
namespace app {
class EmulatorWindow {
class VulkanWindow;
class VulkanRenderer;
using ui::qt::QtWindow;
using ui::Loop;
class EmulatorWindow : public ui::qt::QtWindow {
Q_OBJECT
public:
virtual ~EmulatorWindow();
EmulatorWindow(Loop *loop, const std::string& title);
static std::unique_ptr<EmulatorWindow> Create(Emulator* emulator);
bool Launch(const std::string& path);
Emulator* emulator() const { return emulator_; }
ui::Loop* loop() const { return loop_.get(); }
ui::Window* window() const { return window_.get(); }
xe::Emulator* emulator() { return emulator_.get(); }
void UpdateTitle();
void ToggleFullscreen();
void SetInitializingShaderStorage(bool initializing);
protected:
// Events
private slots:
private:
explicit EmulatorWindow(Emulator* emulator);
void CreateMenuBar();
bool Initialize();
bool InitializeVulkan();
void FileDrop(const std::filesystem::path& filename);
void FileOpen();
void FileClose();
void ShowContentDirectory();
void CheckHideCursor();
void CpuTimeScalarReset();
void CpuTimeScalarSetHalf();
void CpuTimeScalarSetDouble();
void CpuBreakIntoDebugger();
void CpuBreakIntoHostDebugger();
void GpuTraceFrame();
void GpuClearCaches();
void ShowHelpWebsite();
void ShowCommitID();
std::unique_ptr<xe::Emulator> emulator_;
Emulator* emulator_;
std::unique_ptr<ui::Loop> loop_;
std::unique_ptr<ui::Window> window_;
std::string base_title_;
uint64_t cursor_hide_time_ = 0;
bool initializing_shader_storage_ = false;
std::unique_ptr<QWindow> graphics_window_;
std::unique_ptr<ui::GraphicsProvider> graphics_provider_;
std::unique_ptr<hid::InputSystem> input_system_;
std::unique_ptr<QVulkanInstance> vulkan_instance_;
};
} // namespace app
} // namespace xe
#endif // XENIA_APP_EMULATOR_WINDOW_H_
#endif // XENIA_UI_QT_MAIN_WINDOW_H_

View File

@ -0,0 +1,107 @@
#include "xenia/app/library/game_entry.h"
namespace xe {
namespace app {
XGameEntry* XGameEntry::from_game_info(const GameInfo& info) {
auto entry = new XGameEntry();
auto result = entry->apply_info(info);
if (!result) return nullptr;
return entry;
};
XGameEntry::XGameEntry() {
format_ = XGameFormat::kUnknown;
ratings_ = xex2_game_ratings_t();
version_.value = 0x0;
base_version_.value = 0x0;
regions_ = XGameRegions::XEX_REGION_ALL;
}
XGameEntry::XGameEntry(const XGameEntry& other) {
format_ = other.format_;
file_path_ = other.file_path_;
file_name_ = other.file_name_;
launch_paths_ = other.launch_paths_;
default_launch_paths_ = other.default_launch_paths_;
title_ = other.title_;
icon_ = other.icon_;
icon_size_ = other.icon_size_;
title_id_ = other.title_id_;
media_id_ = other.media_id_;
alt_title_ids_ = other.alt_title_ids_;
alt_media_ids_ = other.alt_media_ids_;
disc_map_ = other.disc_map_;
version_ = other.version_;
base_version_ = other.base_version_;
ratings_ = other.ratings_;
regions_ = other.regions_;
build_date_ = other.build_date_;
genre_ = other.genre_;
release_date_ = other.release_date_;
player_count_ = other.player_count_;
}
bool XGameEntry::is_valid() {
// Minimum requirements
return !file_path_.empty() && title_id_ && media_id_;
}
bool XGameEntry::is_missing_data() {
return title_.length() == 0 || icon_[0] == 0 || disc_map_.size() == 0;
// TODO: Version
// TODO: Base Version
// TODO: Ratings
// TODO: Regions
}
bool XGameEntry::apply_info(const GameInfo& info) {
auto xex = &info.xex_info;
auto nxe = &info.nxe_info;
format_ = info.format;
file_path_ = info.path;
file_name_ = info.filename;
if (!xex) return false;
title_id_ = xex->execution_info.title_id;
media_id_ = xex->execution_info.media_id;
version_ = xex->execution_info.version.value;
base_version_ = xex->execution_info.base_version.value;
ratings_ = xex->game_ratings;
regions_ = (xex2_region_flags)xe::byte_swap<uint32_t>(
xex->security_info.region.value);
// Add to disc map / launch paths
auto disc_id = xex->execution_info.disc_number;
disc_map_.insert_or_assign(disc_id, media_id_);
launch_paths_.insert_or_assign(info.path, media_id_);
if (!default_launch_paths_.count(media_id_)) {
default_launch_paths_.insert(std::make_pair(media_id_, info.path));
}
if (xex->game_title.length() > 0) {
title_ = xex->game_title;
} else if (nxe && nxe->game_title.length() > 0) {
title_ = nxe->game_title;
}
if (xex->icon) {
delete[] icon_;
icon_size_ = xex->icon_size;
icon_ = (uint8_t*)calloc(1, icon_size_);
memcpy(icon_, xex->icon, icon_size_);
} else if (nxe && nxe->icon) {
delete[] icon_;
icon_size_ = nxe->icon_size;
icon_ = (uint8_t*)calloc(1, icon_size_);
memcpy(icon_, nxe->icon, icon_size_);
}
return true;
}
} // namespace app
} // namespace xe

View File

@ -0,0 +1,81 @@
#ifndef XENIA_APP_GAME_ENTRY_H_
#define XENIA_APP_GAME_ENTRY_H_
#include <map>
#include <vector>
#include "xenia/app/library/scanner_utils.h"
#include "xenia/kernel/util/xex2_info.h"
namespace xe {
namespace app {
class XGameEntry final {
public:
static XGameEntry* from_game_info(const GameInfo& info);
explicit XGameEntry();
explicit XGameEntry(const XGameEntry& other);
bool is_valid();
bool is_missing_data();
bool apply_info(const GameInfo& info);
const XGameFormat& format() const { return format_; }
const std::filesystem::path& file_path() const { return file_path_; }
const std::filesystem::path& file_name() const { return file_name_; }
const std::map<std::filesystem::path, uint32_t> launch_paths() const {
return launch_paths_;
}
const std::map<uint32_t, std::filesystem::path> default_launch_paths() const {
return default_launch_paths_;
}
const std::string& title() const { return title_; }
const uint8_t* icon() const { return icon_; }
const size_t& icon_size() const { return icon_size_; }
const uint32_t title_id() const { return title_id_; }
const uint32_t media_id() const { return media_id_; }
const std::vector<uint32_t>& alt_title_ids() const { return alt_title_ids_; }
const std::vector<uint32_t>& alt_media_ids() const { return alt_media_ids_; }
const std::map<uint8_t, uint32_t>& disc_map() const { return disc_map_; }
const xex2_version& version() const { return version_; }
const xex2_version& base_version() const { return base_version_; }
const xex2_game_ratings_t& ratings() const { return ratings_; }
const xex2_region_flags& regions() const { return regions_; }
const std::string& genre() const { return genre_; }
const std::string& build_date() const { return build_date_; }
const std::string& release_date() const { return release_date_; }
const uint8_t& player_count() const { return player_count_; }
private:
// File Info
XGameFormat format_;
std::filesystem::path file_path_;
std::filesystem::path file_name_;
std::map<std::filesystem::path, uint32_t> launch_paths_; // <Path, MediaId>
std::map<uint32_t, std::filesystem::path>
default_launch_paths_; // <MediaId, Path>
// Game Metadata
std::string title_;
uint8_t* icon_ = nullptr;
size_t icon_size_ = 0;
uint32_t title_id_ = 0;
uint32_t media_id_ = 0;
std::vector<uint32_t> alt_title_ids_;
std::vector<uint32_t> alt_media_ids_;
std::map<uint8_t, uint32_t> disc_map_; // <Disc #, MediaID>
xex2_version version_;
xex2_version base_version_;
xex2_game_ratings_t ratings_;
xex2_region_flags regions_;
std::string build_date_;
std::string genre_;
std::string release_date_;
uint8_t player_count_ = 0;
};
} // namespace app
} // namespace xe
#endif

View File

@ -0,0 +1,107 @@
#include "xenia/app/library/game_library.h"
#include <algorithm>
#include "xenia/app/library/game_scanner.h"
namespace xe {
namespace app {
using AsyncCallback = XGameLibrary::AsyncCallback;
XGameLibrary* XGameLibrary::Instance() {
static XGameLibrary* instance = new XGameLibrary;
return instance;
}
bool XGameLibrary::ContainsPath(const std::filesystem::path& path) const {
auto existing = std::find(paths_.begin(), paths_.end(), path);
if (existing != paths_.end()) {
return false; // Path already exists.
}
return true;
}
bool XGameLibrary::ContainsGame(uint32_t title_id) const {
return FindGame(title_id) != nullptr;
}
const XGameEntry* XGameLibrary::FindGame(const uint32_t title_id) const {
auto result = std::find_if(games_.begin(), games_.end(),
[title_id](const XGameEntry& entry) {
return entry.title_id() == title_id;
});
if (result != games_.end()) {
return &*result;
}
return nullptr;
}
bool XGameLibrary::RemovePath(const std::filesystem::path& path) {
auto existing = std::find(paths_.begin(), paths_.end(), path);
if (existing == paths_.end()) {
return false; // Path does not exist.
}
paths_.erase(existing);
return true;
}
int XGameLibrary::ScanPath(const std::filesystem::path& path) {
int count = 0;
AddPath(path);
const auto& results = XGameScanner::ScanPath(path);
for (const XGameEntry& result : results) {
count++;
AddGame(result);
}
return count;
}
int XGameLibrary::ScanPathAsync(const std::filesystem::path& path,
const AsyncCallback& cb) {
AddPath(path);
auto paths = XGameScanner::FindGamesInPath(path);
int count = static_cast<int>(paths.size());
return XGameScanner::ScanPathsAsync(
paths, [=](const XGameEntry& entry, int scanned) {
AddGame(entry);
if (cb) {
cb(((double)scanned / count) * 100.0, entry);
}
});
}
void XGameLibrary::AddGame(const XGameEntry& game) {
std::lock_guard<std::mutex> lock(mutex_);
uint32_t title_id = game.title_id();
const auto& begin = games_.begin();
const auto& end = games_.end();
auto result = end;
if (title_id != 0x00000000) {
result = std::find_if(begin, end, [title_id](const XGameEntry& entry) {
return entry.title_id() == title_id;
});
}
// title already exists in library, overwrite existing
if (result != games_.end()) {
*result = game;
} else {
games_.push_back(game);
}
}
void XGameLibrary::AddPath(const std::filesystem::path& path) {
auto result = std::find(paths_.begin(), paths_.end(), path);
// only save unique paths
if (result == paths_.end()) {
paths_.push_back(path);
}
}
} // namespace app
} // namespace xe

View File

@ -0,0 +1,70 @@
#ifndef XENIA_APP_GAME_LIBRARY_H_
#define XENIA_APP_GAME_LIBRARY_H_
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "xenia/app/library/game_entry.h"
#include "xenia/app/library/game_scanner.h"
#include "xenia/app/library/scanner_utils.h"
namespace xe {
namespace app {
class XGameLibrary {
public:
using AsyncCallback = std::function<void(double, const XGameEntry&)>;
XGameLibrary(XGameLibrary const&) = delete;
XGameLibrary& operator=(XGameLibrary const&) = delete;
static XGameLibrary* Instance();
// Returns whether the library has scanned the provided path.
bool ContainsPath(const std::filesystem::path& path) const;
// Returns whether the library has scanned a game with the provided title id.
bool ContainsGame(uint32_t title_id) const;
// Searches the library for a scanned game entry, and returns it if present.
const XGameEntry* FindGame(const uint32_t title_id) const;
// Scans path for supported games/apps synchronously.
// Returns the number of games found.
int ScanPath(const std::filesystem::path& path);
// Scans path for supported games/apps asynchronously,
// calling the provided callback for each game found.
// Returns the number of games found.
int ScanPathAsync(const std::filesystem::path& path, const AsyncCallback& cb);
// Remove a path from the library.
// If path is found, it is removed and the library is rescanned.
bool RemovePath(const std::filesystem::path& path);
// Add a manually crafted game entry to the library.
void AddGame(const XGameEntry& game);
const std::vector<XGameEntry>& games() const { return games_; }
const size_t size() const { return games_.size(); }
void Clear() { games_.clear(); }
// TODO: Provide functions to load and save the library from a cache on disk.
private:
void AddPath(const std::filesystem::path& path);
XGameLibrary() = default;
std::vector<XGameEntry> games_;
std::vector<std::filesystem::path> paths_;
std::mutex mutex_;
};
} // namespace app
} // namespace xe
#endif

View File

@ -0,0 +1,254 @@
#include "xenia/app/library/game_scanner.h"
#include "xenia/app/library/scanner_utils.h"
#include "xenia/base/logging.h"
#include "xenia/base/threading.h"
#include <deque>
namespace xe {
namespace app {
using filesystem::FileInfo;
using AsyncCallback = XGameScanner::AsyncCallback;
std::vector<wstring> XGameScanner::FindGamesInPath(const wstring& path) {
// Path is a directory, scan recursively
// TODO: Warn about recursively scanning paths with large hierarchies
std::deque<wstring> queue;
queue.push_front(path);
std::vector<wstring> paths;
int game_count = 0;
while (!queue.empty()) {
wstring current_path = queue.front();
FileInfo current_file;
filesystem::GetInfo(current_path, &current_file);
queue.pop_front();
if (current_file.type == FileInfo::Type::kDirectory) {
std::vector<FileInfo> directory_files =
filesystem::ListFiles(current_path);
for (FileInfo file : directory_files) {
if (CompareCaseInsensitive(file.name, L"$SystemUpdate")) continue;
auto next_path = (current_path, file.name);
// Skip searching directories with an extracted default.xex file
if (std::filesystem::exists(next_path / L"default.xex")) {
queue.push_front(next_path / L"default.xex");
continue;
}
queue.push_front(next_path);
}
} else {
// Exclusively scan iso, xex, or files without an extension.
auto extension = GetFileExtension(current_path);
if (!extension.empty() && extension != L"xex" && extension != L"iso") {
continue;
}
// Do not attempt to scan SVOD data files
auto filename = GetFileName(current_path);
if (memcmp(filename.c_str(), L"Data", 4) == 0) continue;
// Skip empty files
if (current_file.total_size == 0) continue;
paths.push_back(current_path);
game_count++;
}
}
return paths;
}
std::vector<XGameEntry> XGameScanner::ScanPath(const wstring& path) {
std::vector<XGameEntry> games;
// Check if the given path exists
if (!std::filesystem::exists(path)) {
return games;
}
// Scan if the given path is a file
if (!std::filesystem::is_directory(path)) {
XGameEntry game_entry;
if (XFAILED(ScanGame(path, &game_entry))) {
//XELOGE("Failed to scan game at {}", xe::path_to_utf8(path));
} else {
games.emplace_back(std::move(game_entry));
}
} else {
const std::vector<wstring>& game_paths = FindGamesInPath(path);
for (const wstring& game_path : game_paths) {
XGameEntry game_entry;
if (XFAILED(ScanGame(game_path, &game_entry))) {
continue;
}
games.emplace_back(std::move(game_entry));
}
}
//XELOGI("Scanned {} files", games.size());
return games;
}
int XGameScanner::ScanPathAsync(const wstring& path, const AsyncCallback& cb) {
std::vector<wstring> paths = {path};
return ScanPathsAsync(paths, cb);
}
int XGameScanner::ScanPathsAsync(const std::vector<wstring>& paths,
const AsyncCallback& cb) {
// start scanning in a new thread
// TODO: switch to xe::threading::Thread instead of std::thread?
std::thread scan_thread = std::thread(
[](std::vector<wstring> paths, AsyncCallback cb) {
std::atomic<int> scanned = 0;
auto scan_func = [&](const std::vector<wstring>& paths, size_t start,
size_t size) {
for (auto it = paths.begin() + start;
it != paths.begin() + start + size; ++it) {
scanned++;
XGameEntry game_info;
auto status = ScanGame(*it, &game_info);
if (cb && XSUCCEEDED(status)) {
cb(game_info, scanned);
} else {
//XELOGE("Failed to scan game at {}", it);
}
}
};
uint32_t thread_count = xe::threading::logical_processor_count();
std::vector<std::thread> threads;
// scan games on this thread if the user has < 4 cores
if (thread_count < 4) {
scan_func(paths, 0, paths.size());
} else {
// split workload into even amounts based on core count
size_t total_size = paths.size();
size_t work_size = paths.size() / thread_count;
size_t leftover = paths.size() % thread_count;
if (work_size > 0) {
for (uint32_t i = 0; i < thread_count; i++) {
threads.emplace_back(std::move(
std::thread(scan_func, paths, i * work_size, work_size)));
}
}
scan_func(paths, total_size - leftover, leftover);
}
for (auto& thread : threads) {
thread.join();
}
},
paths, cb);
scan_thread.detach();
return (int)paths.size();
}
X_STATUS XGameScanner::ScanGame(const std::filesystem::path& path,
XGameEntry* out_info) {
if (!out_info) {
return X_STATUS_UNSUCCESSFUL;
}
if (!std::filesystem::exists(path)) {
return X_STATUS_UNSUCCESSFUL;
}
XGameFormat format = ResolveFormat(path);
const char* format_str;
switch (format) {
case XGameFormat::kIso: {
format_str = "ISO";
break;
}
case XGameFormat::kStfs: {
format_str = "STFS";
break;
}
case XGameFormat::kXex: {
format_str = "XEX";
break;
}
default: {
format_str = "Unknown";
break;
}
}
//XELOGD("Scanning {}", std::filesystem::absolute(path));
auto device = CreateDevice(path);
if (device == nullptr || !device->Initialize()) {
return X_STATUS_UNSUCCESSFUL;
}
XELOGI("Format is {}", format_str);
GameInfo game_info;
game_info.filename = GetFileName(path);
game_info.path = path;
game_info.format = format;
// Read XEX
auto xex_entry = device->ResolvePath("default.xex");
if (xex_entry) {
File* xex_file = nullptr;
auto status = xex_entry->Open(vfs::FileAccess::kFileReadData, &xex_file);
if (XSUCCEEDED(status)) {
status = XexScanner::ScanXex(xex_file, &game_info);
if (!XSUCCEEDED(status)) {
XELOGE("Could not parse xex file: {}",
xex_file->entry()->path().c_str());
return status;
}
} else {
XELOGE("Could not load default.xex from device: {}", status);
return X_STATUS_UNSUCCESSFUL;
}
xex_file->Destroy();
} else {
XELOGE("Could not resolve default.xex");
return X_STATUS_UNSUCCESSFUL;
}
// Read NXE
auto nxe_entry = device->ResolvePath("nxeart");
if (nxe_entry) {
File* nxe_file = nullptr;
auto status = nxe_entry->Open(vfs::FileAccess::kFileReadData, &nxe_file);
if (XSUCCEEDED(status)) {
status = NxeScanner::ScanNxe(nxe_file, &game_info);
if (!XSUCCEEDED(status)) {
XELOGE("Could not parse nxeart file: {}",
nxe_file->entry()->path());
return status;
}
} else {
XELOGE("Could not load nxeart from device: %x", status);
}
nxe_file->Destroy();
} else {
XELOGI("Game does not have an nxeart file");
}
out_info->apply_info(game_info);
return X_STATUS_SUCCESS;
}
} // namespace app
} // namespace xe

View File

@ -0,0 +1,48 @@
#ifndef XENIA_APP_GAME_SCANNER_H_
#define XENIA_APP_GAME_SCANNER_H_
#include <functional>
#include <thread>
#include <vector>
#include "xenia/app/library/game_entry.h"
#include "xenia/app/library/nxe_scanner.h"
#include "xenia/app/library/scanner_utils.h"
#include "xenia/app/library/xex_scanner.h"
namespace xe {
namespace app {
class XGameLibrary;
class XGameScanner {
public:
using AsyncCallback = std::function<void(const XGameEntry&, int)>;
// Returns a vector of all supported games in provided path.
static std::vector<wstring> FindGamesInPath(const wstring& path);
// Scans a provided path and recursively parses the games.
// Returns a vector of parsed game entries.
static std::vector<XGameEntry> ScanPath(const wstring& path);
// Scans a provided path and recursively parses the games asynchronously.
// The callback provided is called on each successfully parsed game.
// Returns the number of games found in the path.
static int ScanPathAsync(const wstring& path,
const AsyncCallback& cb = nullptr);
// Scans a list of provided paths and recursively parses the games
// asynchronously. The callback provided is called on each successfully parsed
// game. Returns the number of games found in all paths provided.
static int ScanPathsAsync(const std::vector<wstring>& paths,
const AsyncCallback& cb = nullptr);
// Scans a path for a single game, populating the provided output game entry
// if found and parsed successfully.
static X_STATUS ScanGame(const std::filesystem::path& path,
XGameEntry* out_info);
};
} // namespace app
} // namespace xe
#endif

View File

@ -0,0 +1,38 @@
#include "xenia/app/library/nxe_scanner.h"
#include "xenia/app/library/scanner_utils.h"
#include "xenia/vfs/devices/stfs_container_entry.h"
namespace xe {
namespace app {
using vfs::StfsHeader;
X_STATUS NxeScanner::ScanNxe(File* file, GameInfo* out_info) {
NxeInfo* nxe_info = &out_info->nxe_info;
// Read Header
size_t file_size = file->entry()->size();
uint8_t* data = new uint8_t[file_size];
Read(file, &data[0]);
StfsHeader header;
header.Read(data);
// Read Title
std::string title(xe::to_utf8(header.title_name));
nxe_info->game_title = title;
// Read Icon
nxe_info->icon_size = header.title_thumbnail_image_size;
nxe_info->icon = (uint8_t*)calloc(1, nxe_info->icon_size);
memcpy(nxe_info->icon, header.title_thumbnail_image, nxe_info->icon_size);
// TODO: Read nxebg.jpg
// TODO: Read nxeslot.jpg
// How can we open the file with a StfsContainerDevice?
delete[] data;
return X_STATUS_SUCCESS;
}
} // namespace app
} // namespace xe

View File

@ -0,0 +1,20 @@
#ifndef XENIA_APP_NXE_SCANNER_H_
#define XENIA_APP_NXE_SCANNER_H_
#include "xenia/app/library/scanner_utils.h"
#include "xenia/vfs/file.h"
namespace xe {
namespace app {
using vfs::File;
class NxeScanner {
public:
static X_STATUS ScanNxe(File* file, GameInfo* out_info);
};
} // namespace app
} // namespace xe
#endif

View File

@ -0,0 +1,205 @@
#ifndef XENIA_APP_SCANNER_UTILS_H_
#define XENIA_APP_SCANNER_UTILS_H_
#include "xenia/base/filesystem.h"
#include "xenia/base/string_util.h"
#include "xenia/kernel/util/xex2_info.h"
#include "xenia/vfs/device.h"
#include "xenia/vfs/devices/disc_image_device.h"
#include "xenia/vfs/devices/host_path_device.h"
#include "xenia/vfs/devices/stfs_container_device.h"
#include "xenia/vfs/file.h"
#include <algorithm>
#include <fstream>
#include <iostream>
#include <string>
namespace xe {
namespace app {
using std::wstring;
using vfs::Device;
using vfs::File;
enum XGameFormat {
kUnknown,
kIso,
kStfs,
kXex,
};
typedef xex2_header XexHeader;
typedef xex2_opt_header XexOptHeader;
typedef xex2_game_ratings_t XGameRatings;
typedef xex2_region_flags XGameRegions;
typedef xe_xex2_version_t XGameVersion;
struct XexInfo {
std::string game_title;
uint8_t* icon = nullptr;
size_t icon_size;
uint32_t* alt_title_ids = nullptr;
uint32_t alt_title_ids_count;
uint32_t base_address;
xex2_opt_execution_info execution_info;
xex2_opt_file_format_info* file_format_info = nullptr;
xex2_game_ratings_t game_ratings;
uint32_t header_count;
uint32_t header_size;
xex2_module_flags module_flags;
xex2_multi_disc_media_id_t* multi_disc_media_ids = nullptr;
uint32_t multi_disc_media_ids_count;
xex2_opt_original_pe_name* original_pe_name = nullptr;
xex2_page_descriptor* page_descriptors = nullptr;
uint32_t page_descriptors_count;
xex2_resource* resources = nullptr;
uint32_t resources_count;
xex2_security_info security_info;
uint32_t security_offset;
uint8_t session_key[0x10];
xex2_system_flags system_flags;
~XexInfo() {
delete[] icon;
delete[] alt_title_ids;
delete file_format_info;
delete[] multi_disc_media_ids;
delete original_pe_name;
delete[] page_descriptors;
delete[] resources;
}
};
struct NxeInfo {
std::string game_title;
uint8_t* icon = nullptr;
size_t icon_size;
uint8_t* nxe_background_image = nullptr; // TODO
size_t nxe_background_image_size; // TODO
uint8_t* nxe_slot_image = nullptr; // TODO
size_t nxe_slot_image_size; // TODO
NxeInfo() = default;
~NxeInfo() {
delete[] icon;
delete[] nxe_background_image;
delete[] nxe_slot_image;
}
};
struct GameInfo {
XGameFormat format;
std::filesystem::path path;
std::filesystem::path filename;
NxeInfo nxe_info;
XexInfo xex_info;
GameInfo() = default;
};
inline const bool CompareCaseInsensitive(const wstring& left,
const wstring& right) {
// Copy strings for transform
wstring a(left);
wstring b(right);
std::transform(a.begin(), a.end(), a.begin(), toupper);
std::transform(b.begin(), b.end(), b.begin(), toupper);
return a == b;
}
inline const std::filesystem::path GetFileExtension(
const std::filesystem::path& path) {
return path.extension();
}
inline const std::filesystem::path GetFileName(const std::filesystem::path& path) {
return path.filename();
}
inline const std::filesystem::path GetParentDirectory(
const std::filesystem::path& path) {
return path.parent_path();
}
inline X_STATUS Read(File* file, void* buffer, size_t offset = 0,
size_t length = 0) {
if (!buffer) {
return X_STATUS_UNSUCCESSFUL;
}
if (!length) {
length = file->entry()->size();
}
size_t bytes_read;
file->ReadSync(buffer, length, offset, &bytes_read);
if (length == bytes_read) {
return X_STATUS_SUCCESS;
}
return X_STATUS_UNSUCCESSFUL;
}
inline void ReadStfsMagic(const std::filesystem::path& path, char out[4]) {
using namespace xe::filesystem;
auto file = OpenFile(path, "r");
if (file) {
size_t read = 0;
fread(out, 4, 1, file);
fclose(file);
}
}
inline const XGameFormat ResolveFormat(const std::filesystem::path& path) {
const std::wstring& extension = GetFileExtension(path);
if (CompareCaseInsensitive(extension, L"iso")) return XGameFormat::kIso;
if (CompareCaseInsensitive(extension, L"xex")) return XGameFormat::kXex;
// STFS Container
char magic[4];
if (extension.length() == 0) {
ReadStfsMagic(path, magic);
if (memcmp(&magic, "LIVE", 4) == 0 || memcmp(&magic, "CON ", 4) == 0 ||
memcmp(&magic, "PIRS", 4) == 0)
return XGameFormat::kStfs;
}
return XGameFormat::kUnknown;
}
inline std::unique_ptr<Device> CreateDevice(const std::filesystem::path& path) {
std::string mount_path = "\\SCAN";
XGameFormat format = ResolveFormat(path);
switch (format) {
case XGameFormat::kIso:
return std::make_unique<vfs::DiscImageDevice>(mount_path, path);
case XGameFormat::kXex:
return std::make_unique<vfs::HostPathDevice>(
mount_path, GetParentDirectory(path), true);
case XGameFormat::kStfs:
return std::make_unique<vfs::StfsContainerDevice>(mount_path, path);
default:
return nullptr;
}
}
template <typename T>
inline T Read(File* file, size_t offset) {
uint8_t data[sizeof(T)];
Read(file, data, offset, sizeof(T));
T swapped = xe::load_and_swap<T>(data);
return swapped;
}
} // namespace app
} // namespace xe
#endif // !define XENIA_APP_SCANNER_UTILS_H_

View File

@ -0,0 +1,555 @@
#include "xenia/app/library/xex_scanner.h"
#include "third_party/crypto/TinySHA1.hpp"
#include "third_party/crypto/rijndael-alg-fst.h"
#include "xenia/app/library/scanner_utils.h"
#include "xenia/base/logging.h"
#include "xenia/cpu/lzx.h"
namespace xe {
namespace app {
void aes_decrypt_buffer(const uint8_t* session_key, const uint8_t* input_buffer,
const size_t input_size, uint8_t* output_buffer,
const size_t output_size) {
XELOGI("AesDecryptBuffer called on input of size %d", input_size);
uint32_t rk[4 * (MAXNR + 1)];
uint8_t ivec[16] = {0};
int32_t Nr = rijndaelKeySetupDec(rk, session_key, 128);
const uint8_t* ct = input_buffer;
uint8_t* pt = output_buffer;
for (size_t n = 0; n < input_size; n += 16, ct += 16, pt += 16) {
// Decrypt 16 uint8_ts from input -> output.
rijndaelDecrypt(rk, Nr, ct, pt);
for (size_t i = 0; i < 16; i++) {
// XOR with previous.
pt[i] ^= ivec[i];
// Set previous.
ivec[i] = ct[i];
}
}
}
void aes_decrypt_inplace(const uint8_t* session_key, const uint8_t* buffer,
const size_t size) {
XELOGI("AesDecryptInplace called on input of size %d", size);
uint32_t rk[4 * (MAXNR + 1)];
uint8_t ivec[0x10] = {0};
int32_t Nr = rijndaelKeySetupDec(rk, session_key, 128);
for (size_t n = 0; n < size; n += 0x10) {
uint8_t* in = (uint8_t*)buffer + n;
uint8_t out[0x10] = {0};
rijndaelDecrypt(rk, Nr, in, out);
for (size_t i = 0; i < 0x10; i++) {
// XOR with previous.
out[i] ^= ivec[i];
// Set previous.
ivec[i] = in[i];
}
// Fast copy
*(size_t*)in = *(size_t*)out;
*(size_t*)(in + 0x8) = *(size_t*)(out + 0x8);
}
}
X_STATUS ReadXexImageUncompressed(File* file, XexInfo* info, size_t offset,
uint32_t length, uint8_t*& out_data) {
XELOGI("Reading uncompressed image");
size_t file_size = file->entry()->size();
auto format = info->file_format_info;
const size_t exe_size = file_size - info->header_size;
X_STATUS status;
switch (format->encryption_type) {
case XEX_ENCRYPTION_NONE: {
status = Read(file, out_data, info->header_size + offset, length);
if (status != X_STATUS_SUCCESS) {
XELOGE("Could not read from file %ls", file->entry()->path().c_str());
return status;
}
XELOGI("Successfully read unencrypted uncompressed image.");
return X_STATUS_SUCCESS;
}
case XEX_ENCRYPTION_NORMAL: {
status = Read(file, out_data, info->header_size + offset, length);
if (status != X_STATUS_SUCCESS) {
XELOGE("Could not read from file %ls", file->entry()->path().c_str());
return status;
}
aes_decrypt_inplace(info->session_key, out_data, length);
XELOGI("Successfully read encrypted uncompressed image.");
return X_STATUS_SUCCESS;
}
default:
XELOGI("Could not read image. Unknown encryption type.");
return X_STATUS_UNSUCCESSFUL;
}
}
X_STATUS ReadXexImageBasicCompressed(File* file, XexInfo* info, size_t offset,
uint32_t length, uint8_t*& out_data) {
XELOGI("Reading basic compressed image.");
auto file_size = file->entry()->size();
auto format = info->file_format_info;
auto compression = &format->compression_info.basic;
auto encryption = format->encryption_type;
// Find proper block
uint32_t i;
uint32_t compressed_position = 0;
uint32_t uncompressed_position = 0;
uint32_t block_count = (format->info_size - 8) / 8;
for (i = 0; i < block_count; i++) {
const uint32_t data_size = compression->blocks[i].data_size;
const uint32_t zero_size = compression->blocks[i].zero_size;
const uint32_t total_size = data_size + zero_size;
if (uncompressed_position + total_size > offset) break;
compressed_position += data_size;
uncompressed_position += total_size;
}
XELOGI("Found offset %08x at block %d", compressed_position, i);
// For some reason the AES IV is screwing up the first 0x10 bytes,
// so in the meantime, we're just shifting back 0x10 and skipping
// the garbage data.
auto block = compression->blocks[i];
uint32_t block_size = block.data_size + 0x10;
uint32_t block_address = info->header_size + compressed_position - 0x10;
uint8_t* data = new uint8_t[file->entry()->size()];
Read(file, data, block_address, block_size);
if (encryption == XEX_ENCRYPTION_NORMAL) {
XELOGI("Decrypting basic compressed image.");
aes_decrypt_inplace(info->session_key, data, block_size);
}
// Get the requested data
auto remaining_offset = offset - uncompressed_position;
out_data = (uint8_t*)malloc(length);
memcpy(out_data, data + remaining_offset + 0x10, length);
XELOGI("Successfully read basic compressed image.");
delete[] data;
return X_STATUS_SUCCESS;
}
X_STATUS ReadXexImageNormalCompressed(File* file, XexInfo* info, size_t offset,
uint32_t length, uint8_t*& out_data) {
auto encryption_type = info->file_format_info->encryption_type;
auto uncompressed_length = info->security_info.image_size;
auto compression_info = info->file_format_info->compression_info.normal;
sha1::SHA1 s;
auto in_length = file->entry()->size() - info->header_size;
uint8_t* in_buffer = new uint8_t[in_length];
Read(file, in_buffer, info->header_size, in_length);
if (encryption_type == XEX_ENCRYPTION_NORMAL)
aes_decrypt_inplace(info->session_key, in_buffer, in_length);
uint8_t* compressed_buffer = new uint8_t[in_length];
uint8_t block_calced_digest[0x14];
auto current_block = &compression_info.first_block;
auto in_cursor = in_buffer;
auto out_cursor = compressed_buffer;
while (current_block->block_size) {
auto next_ptr = in_cursor + current_block->block_size;
auto next_block = (xex2_compressed_block_info*)in_cursor;
s.reset();
s.processBytes(in_cursor, current_block->block_size);
s.finalize(block_calced_digest);
if (memcmp(block_calced_digest, current_block->block_hash, 0x14) != 0) {
delete[] in_buffer;
delete[] compressed_buffer;
return X_STATUS_UNSUCCESSFUL;
}
in_cursor += 0x18;
while (true) {
auto chunk_size = (in_cursor[0] << 8) | in_cursor[1];
in_cursor += 2;
if (!chunk_size) break;
memcpy(out_cursor, in_cursor, chunk_size);
in_cursor += chunk_size;
out_cursor += chunk_size;
}
in_cursor = next_ptr;
current_block = next_block;
}
// Decompress
auto window_size = compression_info.window_size;
auto decompressed_buffer = new uint8_t[uncompressed_length];
lzx_decompress(compressed_buffer, out_cursor - compressed_buffer,
decompressed_buffer, uncompressed_length, window_size, nullptr,
0);
out_data = (uint8_t*)malloc(length);
memcpy(out_data, decompressed_buffer + offset, length);
delete[] in_buffer;
delete[] compressed_buffer;
delete[] decompressed_buffer;
return X_STATUS_SUCCESS;
}
X_STATUS ReadXexImage(File* file, XexInfo* info, size_t offset, uint32_t length,
uint8_t*& out_data) {
auto format = info->file_format_info;
switch (format->compression_type) {
case XEX_COMPRESSION_NONE:
return ReadXexImageUncompressed(file, info, offset, length, out_data);
case XEX_COMPRESSION_BASIC:
return ReadXexImageBasicCompressed(file, info, offset, length, out_data);
case XEX_COMPRESSION_NORMAL:
return ReadXexImageNormalCompressed(file, info, offset, length, out_data);
default:
return X_STATUS_UNSUCCESSFUL;
}
}
inline void ReadXexAltTitleIds(uint8_t* data, XexInfo* info) {
uint32_t length = xe::load_and_swap<uint32_t>(data);
uint32_t count = (length - 0x04) / 0x04;
info->alt_title_ids_count = count;
info->alt_title_ids = new uint32_t[length];
uint8_t* cursor = data + 0x04;
for (uint32_t i = 0; i < length; i++, cursor += 0x04) {
info->alt_title_ids[i] = xe::load_and_swap<uint32_t>(cursor);
}
XELOGI("%d alternate title ids found", count);
}
inline void ReadXexExecutionInfo(uint8_t* data, XexInfo* info) {
uint32_t length = sizeof(xex2_opt_execution_info);
memcpy(&info->execution_info, data, length);
XELOGI("Read ExecutionInfo. TitleID: {}, MediaID: {}",
xe::string_util::to_hex_string(info->execution_info.title_id),
xe::string_util::to_hex_string(info->execution_info.media_id));
}
inline void ReadXexFileFormatInfo(uint8_t* data, XexInfo* info) {
uint32_t length = xe::load_and_swap<uint32_t>(data); // TODO
info->file_format_info = (xex2_opt_file_format_info*)calloc(1, length);
memcpy(info->file_format_info, data, length);
XELOGI("Read FileFormatInfo. Encryption: {}, Compression: {}",
info->file_format_info->encryption_type == XEX_ENCRYPTION_NORMAL
? "NORMAL"
: "NONE",
info->file_format_info->compression_type == XEX_COMPRESSION_NORMAL
? "NORMAL"
: info->file_format_info->compression_type == XEX_COMPRESSION_BASIC
? "BASIC"
: "NONE");
}
inline void ReadXexGameRatings(uint8_t* data, XexInfo* info) {
uint32_t length = 0xC;
memcpy(&info->game_ratings, data, 0xC);
XELOGI("Read GameRatings.");
}
inline void ReadXexMultiDiscMediaIds(uint8_t* data, XexInfo* info) {
uint32_t entry_size = sizeof(xex2_multi_disc_media_id_t);
uint32_t length = xe::load_and_swap<uint32_t>(data);
uint32_t count = (length - 0x04) / 0x10;
info->multi_disc_media_ids_count = count;
info->multi_disc_media_ids =
(xex2_multi_disc_media_id_t*)calloc(count, entry_size);
uint8_t* cursor = data + 0x04;
for (uint32_t i = 0; i < count; i++, cursor += 0x10) {
auto id = &info->multi_disc_media_ids[i];
memcpy(id->hash, cursor, 0x0C);
id->media_id = xe::load_and_swap<uint32_t>(cursor + 0x0C);
}
XELOGI("Read %d MultiDisc Media IDs. Disc is %d of %d", count,
info->execution_info.disc_number, info->execution_info.disc_count);
}
inline void ReadXexOriginalPeName(uint8_t* data, XexInfo* info) {
uint32_t length = xe::load_and_swap<uint32_t>(data);
info->original_pe_name = (xex2_opt_original_pe_name*)calloc(1, length);
memcpy(info->original_pe_name, data, length);
XELOGI("Read OriginalPeName: {}", info->original_pe_name->name);
}
inline void ReadXexResourceInfo(uint8_t* data, XexInfo* info) {
uint32_t size = sizeof(xex2_opt_resource_info);
uint32_t length = xe::load_and_swap<uint32_t>(data);
uint32_t count = (length - 0x04) / 0x10;
info->resources_count = count;
info->resources = (xex2_resource*)calloc(count, size);
memcpy(info->resources, data + 0x4, count * size);
XELOGI("Read %d resource infos", count);
/*uint8_t* cursor = data + 0x04;
for (uint32_t i = 0; i < count; i++, cursor += 0x10) {
auto resource_info = &info->resources[i];
memcpy(resource_info->name, cursor, 0x08);
resource_info->address = xe::load_and_swap<uint32_t>(cursor + 0x08);
resource_info->size = xe::load_and_swap<uint32_t>(cursor + 0x0C);
}*/
}
inline void ReadXexOptHeader(xex2_opt_header* entry, uint8_t* data,
XexInfo* info) {
switch (entry->key) {
case XEX_HEADER_ALTERNATE_TITLE_IDS:
ReadXexAltTitleIds(data + entry->offset, info);
break;
case XEX_HEADER_EXECUTION_INFO:
ReadXexExecutionInfo(data + entry->offset, info);
break;
case XEX_HEADER_FILE_FORMAT_INFO:
ReadXexFileFormatInfo(data + entry->offset, info);
break;
case XEX_HEADER_GAME_RATINGS:
ReadXexGameRatings(data + entry->offset, info);
break;
case XEX_HEADER_MULTIDISC_MEDIA_IDS:
ReadXexMultiDiscMediaIds(data + entry->offset, info);
break;
case XEX_HEADER_RESOURCE_INFO:
ReadXexResourceInfo(data + entry->offset, info);
break;
case XEX_HEADER_ORIGINAL_PE_NAME:
ReadXexOriginalPeName(data + entry->offset, info);
break;
case XEX_HEADER_IMAGE_BASE_ADDRESS:
info->base_address = entry->value;
break;
case XEX_HEADER_SYSTEM_FLAGS:
info->system_flags =
(xex2_system_flags)xe::byte_swap<uint32_t>(entry->value.value);
break;
}
}
X_STATUS TryKey(const uint8_t* key, uint8_t* magic_block, XexInfo* info,
File* file) {
uint8_t decrypted_block[0x10];
const uint16_t PE_MAGIC = 0x4D5A;
auto compression_type = info->file_format_info->compression_type;
auto aes_key = reinterpret_cast<uint8_t*>(info->security_info.aes_key);
aes_decrypt_buffer(key, aes_key, 0x10, info->session_key, 0x10);
aes_decrypt_buffer(info->session_key, magic_block, 0x10, decrypted_block,
0x10);
memcpy(info->session_key, info->session_key, 0x10);
// Decompress if the XEX is lzx compressed
if (compression_type == XEX_COMPRESSION_NORMAL) {
uint8_t* data;
if (XFAILED(ReadXexImageNormalCompressed(file, info, 0, 0x10, data)))
return X_STATUS_UNSUCCESSFUL;
memcpy(decrypted_block, data, 0x10);
delete[] data;
}
uint16_t found_magic = xe::load_and_swap<uint16_t>(decrypted_block);
if (found_magic == PE_MAGIC) return X_STATUS_SUCCESS;
return X_STATUS_UNSUCCESSFUL;
}
X_STATUS ReadSessionKey(File* file, XexInfo* info) {
const uint16_t PE_MAGIC = 0x4D5A;
uint8_t magic_block[0x10];
Read(file, magic_block, info->header_size, 0x10);
// Check if the XEX is already decrypted.
// If decrypted with a third party tool like xextool, the encryption flag
// is not changed, but the data is decrypted.
uint16_t found_magic = xe::load_and_swap<uint16_t>(magic_block);
if (found_magic == PE_MAGIC) {
info->file_format_info->encryption_type = XEX_ENCRYPTION_NONE;
return X_STATUS_SUCCESS;
}
// XEX is encrypted, derive the session key.
if (XFAILED(TryKey(xex2_retail_key, magic_block, info, file)))
if (XFAILED(TryKey(xex2_devkit_key, magic_block, info, file))) {
return X_STATUS_SUCCESS;
}
return X_STATUS_SUCCESS;
}
X_STATUS ReadXexHeaderSecurityInfo(File* file, XexInfo* info) {
const uint32_t length = 0x180;
uint8_t data[length];
Read(file, data, info->security_offset, length);
memcpy(&info->security_info, data, sizeof(xex2_security_info));
// XEX is still encrypted. Derive the session key.
if (XFAILED(ReadSessionKey(file, info))) {
return X_STATUS_UNSUCCESSFUL;
}
return X_STATUS_SUCCESS;
}
X_STATUS ReadXexHeaderSectionInfo(File* file, XexInfo* info) {
uint32_t offset = info->security_offset;
uint32_t count = Read<uint32_t>(file, offset);
uint32_t size = sizeof(xex2_page_descriptor);
uint32_t length = count * size;
uint8_t* data = new uint8_t[length];
Read(file, data, offset, length);
info->page_descriptors_count = count;
info->page_descriptors = (xex2_page_descriptor*)calloc(count, size);
if (!info->page_descriptors || !count) {
XELOGE("No xex page descriptors are present");
delete[] data;
return X_STATUS_UNSUCCESSFUL;
}
uint8_t* cursor = data + 0x04;
for (uint32_t i = 0; i < count; i++) {
auto section = &info->page_descriptors[i];
section->value = xe::load<uint32_t>(cursor + 0x00);
memcpy(section->data_digest, cursor + 0x04, sizeof(section->data_digest));
cursor += 0x04 + sizeof(section->data_digest);
}
XELOGI("Section info successfully read. %d page descriptors found.", count);
delete[] data;
return X_STATUS_SUCCESS;
}
X_STATUS ReadXexHeader(File* file, XexInfo* info) {
uint32_t header_size = Read<uint32_t>(file, 0x8);
uint8_t* data = new uint8_t[header_size];
Read(file, data, 0x0, header_size);
// Read Main Header Data
info->module_flags =
(xex2_module_flags)xe::load_and_swap<uint32_t>(data + 0x04);
info->header_size = xe::load_and_swap<uint32_t>(data + 0x08);
info->security_offset = xe::load_and_swap<uint32_t>(data + 0x10);
info->header_count = xe::load_and_swap<uint32_t>(data + 0x14);
// Read Optional Headers
uint8_t* cursor = data + 0x18;
for (uint32_t i = 0; i < info->header_count; i++, cursor += 0x8) {
auto entry = reinterpret_cast<xex2_opt_header*>(cursor);
ReadXexOptHeader(entry, data, info);
}
if (XFAILED(ReadXexHeaderSecurityInfo(file, info))) {
XELOGE("Could not read xex security info");
delete[] data;
return X_STATUS_UNSUCCESSFUL;
}
if (XFAILED(ReadXexHeaderSectionInfo(file, info))) {
XELOGE("Could not read xex section info");
delete[] data;
return X_STATUS_UNSUCCESSFUL;
}
delete[] data;
return X_STATUS_SUCCESS;
}
X_STATUS ReadXexResources(File* file, XexInfo* info) {
auto resources = info->resources;
if (resources == nullptr) {
XELOGI("XEX has no resources");
return X_STATUS_SUCCESS;
}
XELOGI("Reading %d XEX resources.", info->resources_count);
for (size_t i = 0; i < info->resources_count; i++) {
auto resource = &resources[i];
uint32_t title_id = info->execution_info.title_id;
uint32_t name =
xe::string_util::from_string<uint32_t>(resource->name, true);
XELOGI("Found resource: %X", name);
// Game resources are listed as the TitleID
if (name == title_id) {
uint32_t offset = resource->address - info->base_address;
XELOGI("Found XBDF resource at %08x with size %08x", offset,
resource->size);
uint8_t* data;
if (XFAILED(ReadXexImage(file, info, offset, resource->size, data))) {
XELOGE("Could not read XBDF resource.");
delete[] data;
return X_STATUS_UNSUCCESSFUL;
}
auto xbdf_data = XdbfGameData(data, resource->size);
if (!xbdf_data.is_valid()) {
delete[] data;
XELOGE("XBDF data is invalid.");
return X_STATUS_UNSUCCESSFUL;
}
// Extract Game Title
info->game_title = xbdf_data.title();
// Extract Game Icon
auto icon = xbdf_data.icon();
info->icon_size = icon.size;
info->icon = (uint8_t*)calloc(1, icon.size);
memcpy(info->icon, icon.buffer, icon.size);
// TODO: Extract Achievements
delete[] data;
XELOGI("Successfully read XBDF resource. Game title: {}, icon size: %08x",
info->game_title.c_str(), info->icon_size);
return X_STATUS_SUCCESS;
}
}
return X_STATUS_SUCCESS;
}
X_STATUS XexScanner::ScanXex(File* file, GameInfo* out_info) {
if (XFAILED(ReadXexHeader(file, &out_info->xex_info))) {
XELOGE("ReadXexHeader failed");
return X_STATUS_UNSUCCESSFUL;
}
if (XFAILED(ReadXexResources(file, &out_info->xex_info))) {
XELOGE("ReadXexResources failed");
return X_STATUS_UNSUCCESSFUL;
}
XELOGI("ScanXex was successful");
return X_STATUS_SUCCESS;
}
} // namespace app
} // namespace xe

View File

@ -0,0 +1,33 @@
#ifndef XENIA_APP_XEX_SCANNER_H_
#define XENIA_APP_XEX_SCANNER_H_
#include "xenia/app/library/game_entry.h"
#include "xenia/app/library/scanner_utils.h"
#include "xenia/kernel/util/xdbf_utils.h"
#include "xenia/kernel/util/xex2_info.h"
#include "xenia/vfs/file.h"
#include <string>
namespace xe {
namespace app {
using kernel::util::XdbfGameData;
using vfs::File;
static const uint8_t xex2_retail_key[16] = {0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28,
0xFD, 0xC3, 0x40, 0x58, 0x3F, 0xBB,
0x08, 0x96, 0xBF, 0x91};
static const uint8_t xex2_devkit_key[16] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00};
class XexScanner {
public:
static X_STATUS ScanXex(File* xex_file, GameInfo* out_info);
};
} // namespace app
} // namespace xe
#endif

View File

@ -1,5 +1,6 @@
project_root = "../../.."
include(project_root.."/tools/build")
local qt = premake.extensions.qt
group("src")
project("xenia-app")
@ -39,20 +40,104 @@ project("xenia-app")
"xenia-hid-sdl",
"xenia-kernel",
"xenia-ui",
"xenia-ui-qt",
"xenia-ui-spirv",
"xenia-ui-vulkan",
"xenia-vfs",
"xxhash",
})
-- Setup Qt libraries
qt.enable()
qtmodules{"core", "gui", "widgets"}
qtprefix "Qt5"
configuration {"Checked"}
qtsuffix "d"
configuration {"Debug"}
qtsuffix "d"
configuration {}
if qt.defaultpath ~= nil then
qtpath(qt.defaultpath)
end
-- Qt static configuration (if necessary). Used by AppVeyor.
if os.getenv("QT_STATIC") then
qt.modules["AccessibilitySupport"] = {
name = "AccessibilitySupport",
include = "QtAccessibilitySupport",
}
qt.modules["EventDispatcherSupport"] = {
name = "EventDispatcherSupport",
include = "QtEventDispatcherSupport",
}
qt.modules["FontDatabaseSupport"] = {
name = "FontDatabaseSupport",
include = "QtFontDatabaseSupport",
}
qt.modules["ThemeSupport"] = {
name = "ThemeSupport",
include = "QtThemeSupport",
}
qt.modules["VulkanSupport"] = {
name = "VulkanSupport",
include = "QtVulkanSupport",
}
defines({"QT_STATIC=1"})
configuration {"not Checked"}
links({
"qtmain",
"qtfreetype",
"qtlibpng",
"qtpcre2",
"qtharfbuzz",
})
configuration {"Checked"}
links({
"qtmaind",
"qtfreetyped",
"qtlibpngd",
"qtpcre2d",
"qtharfbuzzd",
})
configuration {}
qtmodules{"AccessibilitySupport", "EventDispatcherSupport", "FontDatabaseSupport", "ThemeSupport", "VulkanSupport"}
libdirs("%{cfg.qtpath}/plugins/platforms")
filter("platforms:Windows")
-- Qt dependencies
links({
"dwmapi",
"version",
"imm32",
"winmm",
"netapi32",
"userenv",
})
configuration {"not Checked"}
links({"qwindows"})
configuration {"Checked"}
links({"qwindowsd"})
configuration {}
filter()
end
filter("platforms:Windows")
entrypoint("wWinMainCRTStartup")
defines({
"XBYAK_NO_OP_NAMES",
"XBYAK_ENABLE_OMITTED_OPERAND",
})
local_platform_files()
recursive_platform_files()
files({
"xenia_main.cc",
"../base/main_"..platform_suffix..".cc",
"../base/main_init_"..platform_suffix..".cc",
-- Qt files
"*.qrc",
})
resincludedirs({
@ -60,8 +145,8 @@ project("xenia-app")
})
filter("platforms:Windows")
files({
"main_resources.rc",
resincludedirs({
project_root,
})
filter("files:../base/main_init_"..platform_suffix..".cc")
@ -92,4 +177,7 @@ project("xenia-app")
debugdir(project_root)
debugargs({
})
debugenvs({
"PATH=%{cfg.qtpath}/bin",
})
end

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,30 @@
QWidget#DebugTab QLabel {
color: $primary;
}
QWidget#DebugTab QScrollArea {
background: transparent;
border: none;
}
QWidget#DebugTab QScrollArea > QWidget > QWidget {
background: transparent;
}
QWidget#sidebarContainer {
background: $tertiary;
min-width: 300px;
max-width: 300px;
}
QWidget#sidebarTitleLabel {
color: $primary;
font-size: 32px;
}
QWidget#sidebarTitle #XSeperator {
background: $dark1;
}
QWidget#navigationContainer {
background: $dark2;
}

View File

@ -0,0 +1,40 @@
QWidget#sidebarContainer {
background: $tertiary;
min-width: 300;
max-width: 300;
}
QWidget#sidebarTitle {
min-height: 140;
max-height: 140;
}
QLabel#sidebarTitleLabel {
color: $primary;
font-size: 40px;
font-weight: normal;
}
QLabel#sidebarSubtitleLabel {
color: $light2;
font-size: 20px;
font-weight: normal;
}
QWidget#XSeparator {
qproperty-thickness: 2;
min-width: 250;
max-width: 250;
background: $dark1;
}
QToolBar#XToolBar {
min-height: 46;
max-height: 46;
}
QLabel#recentGames {
font-family: "Segoe UI";
font-size: 18px;
color: $primary;
}

View File

@ -0,0 +1,111 @@
QMainWindow {
background-color: $background;
}
QLabel#buildLabel {
color: $light2;
padding: 0px 4px 0px 4px;
font-size: 10px;
qproperty-alignment: AlignTop;
}
QMenu {
background-color: $dark1;
border: 1px solid $light3;
}
QMenu::item {
background-color: transparent;
color: $primary;
}
QMenu::item:selected {
background-color: $secondaryDark;
}
/* Global Scrollbar theming */
QScrollBar:horizontal {
background: $dark2;
height: 15px;
margin: 0px 20px 0 20px;
}
QScrollBar::handle:horizontal {
background: $dark1;
min-width: 20px;
}
QScrollBar::add-line:horizontal {
background: $dark2;
width: 20px;
subcontrol-position: right;
subcontrol-origin: margin;
}
QScrollBar::sub-line:horizontal {
background: $dark2;
width: 20px;
subcontrol-position: left;
subcontrol-origin: margin;
}
QScrollBar::add-page:horizontal,
QScrollBar::sub-page:horizontal {
background: none;
}
QScrollBar::left-arrow:horizontal {
image: url(:/resources/graphics/left-arrow.png);
width: 8px;
height: 8px;
}
QScrollBar::right-arrow:horizontal {
image: url(:/resources/graphics/right-arrow.png);
width: 8px;
height: 8px;
}
QScrollBar:vertical {
background: $dark2;
width: 15px;
margin: 20px 0 20px 0;
}
QScrollBar::handle:vertical {
background: $dark1;
min-height: 20px;
}
QScrollBar::add-line:vertical {
background: $dark2;
height: 20px;
subcontrol-position: top;
subcontrol-origin: margin;
}
QScrollBar::sub-line:vertical {
background: $dark2;
height: 20px;
subcontrol-position: bottom;
subcontrol-origin: margin;
}
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical {
background: $dark2;
}
/* up and down are inversed here for some reason */
QScrollBar::up-arrow:vertical {
image: url(:/resources/graphics/down-arrow.png);
width: 8px;
height: 8px;
}
QScrollBar::down-arrow:vertical {
image: url(:/resources/graphics/up-arrow.png);
width: 8px;
height: 8px;
}

View File

@ -0,0 +1,31 @@
QWidget#SettingsTab QLabel {
color: $primary;
}
QWidget#SettingsTab > QScrollArea {
background: transparent;
border: none;
}
QWidget#SettingsTab > QScrollArea > QWidget > QWidget {
background: transparent;
}
QWidget#sidebarContainer {
background: $tertiary;
min-width: 300px;
max-width: 300px;
}
QWidget#sidebarTitleLabel {
color: $primary;
font-size: 32px;
}
QWidget#sidebarTitle #XSeperator {
background: $dark1;
}
QWidget#navigationContainer {
background: $dark2;
}

View File

@ -0,0 +1,18 @@
QWidget#XCard {
background: $dark2;
border-top-right-radius: 2px;
border-top-left-radius: 2px;
}
QLabel {
color: $light1;
}
QListWidget {
background: transparent;
border: none;
}
QListWidget::item {
background: transparent;
border: none;
}

View File

@ -0,0 +1,15 @@
/*
for some reason QSS property values take priority over calling the setter of the property programatically
to work around this users can set the custom property to true before calling any setters
*/
QCheckBox#XCheckBox[custom="false"] {
qproperty-checked_color: $secondary;
}
/*
these two properties cannot be changed programatically, to allow this move them into the above style
*/
QCheckBox#XCheckBox {
qproperty-border_color: $light2;
qproperty-focus_color: $secondary;
}

View File

@ -0,0 +1,50 @@
QComboBox#XComboBox {
border: 2px solid $light3;
background: $dark2;
color: $light2;
padding-left: 4px;
}
QComboBox#XComboBox:pressed,
QComboBox#XComboBox:hover,
QComboBox#XComboBox:focus,
QComboBox#XComboBox:on {
border: 2px solid $secondaryDark;
}
QComboBox#XComboBox::drop-down {
border: 0
}
QComboBox#XComboBox::down-arrow {
margin-right: 4px;
image: url(:/resources/graphics/down-arrow.png);
width: 14px;
height: 14px;
}
QComboBox#XComboBox::item
{
background: $dark2;
}
QComboBox#XComboBox::item:selected
{
background: $secondaryDark;
}
QComboBox#XComboBox::item:checked
{
font-weight: bold;
}
QComboBox#XComboBox QAbstractItemView {
background: $dark2;
color: $light2;
selection-background-color: $secondaryDark;
outline: 0;
}
/* QComboBox#XComboBox QFrame:focus {
background: $secondaryDark;
} */

View File

@ -0,0 +1,51 @@
QToolButton#XDropdownButton {
background: $dark1;
color: $primary;
border: 2px solid $light3;
padding: 4px;
}
QToolButton#XDropdownButton:disabled {
background: $light3;
color: $dark1;
border: none;
}
QToolButton#XDropdownButton:hover:!pressed,
QToolButton#XDropdownButton:focus {
border: 2px solid $secondaryDark;
outline: none;
}
QToolButton#XDropdownButton:pressed {
background: $secondaryDark;
border: none;
}
QToolButton#XDropdownButton[popupMode="1"] {
padding-right: 15px;
}
QToolButton#XDropdownButton::menu-button {
width: 15px;
margin: 2px;
background: $dark2;
border: none;
}
QToolButton#XDropdownButton:disabled,
QToolButton#XDropdownButton::menu-button {
width: 15px;
margin: 2px;
background: $light3;
border: none;
}
QToolButton#XDropdownButton::menu-button:pressed {
background: $secondaryDark;
}
QToolButton#XDropdownButton::menu-arrow:open {
top: -1px;
left: -1px;
}

View File

@ -0,0 +1,15 @@
QGroupBox {
background-color: $accent;
border: transparent;
border-radius: 2px;
margin: 20px 4px 4px 4px; /* leave space at the top for the title */
color: $primary;
font-family: "Segoe UI Semibold";
font-size: 14px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left; /* position at the top left */
background: transparent;
}

View File

@ -0,0 +1,14 @@
QLineEdit#XLineEdit {
background-color: $dark1;
color: $primary;
border: 2px solid $light3;
}
QLineEdit#XLineEdit:focus {
border: 2px solid $secondaryDark;
}
QLineEdit#XLineEdit:disabled {
color: $light2;
border: 2px solid $dark1;
}

View File

@ -0,0 +1,5 @@
QWidget#XNav {
background-color: $accent;
min-height: 60px;
max-height: 60px;
}

View File

@ -0,0 +1,23 @@
QPushButton#XPushButton {
background: $dark1;
color: $primary;
border: 2px solid $light3;
padding: 4px;
}
QPushButton#XPushButton:disabled {
background: $light3;
color: $dark1;
border: none;
}
QPushButton#XPushButton:hover:!pressed,
QPushButton#XPushButton:focus {
border: 2px solid $secondaryDark;
outline: none;
}
QPushButton#XPushButton:pressed {
background: $secondaryDark;
border: none;
}

View File

@ -0,0 +1,15 @@
/*
for some reason QSS property values take priority over calling the setter of the property programatically
to work around this users can set the custom property to true before calling any setters
*/
QRadioButton#XRadioButton[custom="false"] {
qproperty-checked_color: $secondary;
}
/*
these two properties cannot be changed programatically, to allow this move them into the above style
*/
QRadioButton#XRadioButton {
qproperty-border_color: $light2;
qproperty-focus_color: $secondary;
}

View File

@ -0,0 +1,8 @@
QScrollArea#XScrollArea {
background: transparent;
border: none;
}
QScrollArea#XScrollArea > QWidget > QWidget {
background: transparent;
}

View File

@ -0,0 +1,13 @@
QToolBar::separator {
background: $dark1;
min-width: 300;
max-width: 300;
height: 2;
}
QPushButton#XSideBarButton {
max-height: 50;
min-height: 50;
max-width: 300;
min-width: 300;
}

View File

@ -0,0 +1,17 @@
QPushButton {
color: $primary;
border: none;
background-color: rgba(255, 255, 255, 0);
}
QPushButton:hover {
background-color: $dark1;
}
QPushButton:pressed {
background-color: $dark2;
}
QPushButton:checked {
background-color: $dark2;
}

View File

@ -0,0 +1,68 @@
QSlider#XSlider:horizontal {
min-width: 20px;
min-height: 20px;
}
QSlider#XSlider::groove:horizontal {
background: $secondaryDark;
height: 4px;
margin: 0px 1px 0px 1px;
}
QSlider#XSlider::handle:horizontal {
width: 10px;
border-radius: 5px;
background: $secondary;
margin: -3px -1px -3px -1px;
}
QSlider#XSlider::handle:horizontal:focus {
width: 14px;
height: 14px;
line-height: 14px;
border-radius: 7px;
background: $secondary;
margin: -5px -1px -5px -1px;
}
QSlider#XSlider::groove:vertical {
background: $secondaryDark;
width: 4px;
margin: 1px 0px 1px 0px;
}
QSlider#XSlider::handle:vertical {
height: 10px;
border-radius: 5px;
background: $secondary;
margin: -1px -3px -1px -3px;
}
QSlider#XSlider::handle:vertical:focus {
height: 14px;
border-radius: 7px;
background: $secondary;
margin: -1px -5px -1px -5px;
}
/* Uncomment these to make slider color different before the handle to after */
QSlider#XSlider::sub-page:horizontal {
background: $secondaryDark;
margin: 0px 1px 0px 1px;
}
QSlider#XSlider::add-page:horizontal {
background: $light3;
margin: 0px 1px 0px 1px;
}
QSlider#XSlider::add-page:vertical {
background: $secondaryDark;
margin: 1px 0px 1px 0px;
}
QSlider#XSlider::sub-page:vertical {
background: $light3;
margin: 1px 0px 1px 0px;
}

View File

@ -0,0 +1,8 @@
QStatusBar#XStatusBar {
background-color: rgb(40, 40, 40);
color: $light2;
}
QStatusBar#XStatusBar::item {
border: 0px;
}

View File

@ -0,0 +1,8 @@
QWidget#XTab {
}
QLabel#placeholder {
font: ":resources/fonts/segoeui.ttf";
color: $primary;
font-size: 48px;
}

View File

@ -0,0 +1,6 @@
QWidget#XTabSelector {
qproperty-bar_color: $secondary;
qproperty-font_color: $primary;
qproperty-font_size: 24;
qproperty-tab_spacing: 20;
}

View File

@ -0,0 +1,18 @@
QHeaderView#XTableHeader {
background-color: #373737;
}
QHeaderView#XTableHeader::section {
border: 0px;
border-left: 1px solid #7f7f7f;
background-color: #373737;
color: $primary;
font-size: 12px;
font-weight: 500;
margin: 3px 0;
padding: 0 5px;
}
QHeaderView#XTableHeader::section:first {
border: 0px;
}

View File

@ -0,0 +1,17 @@
QTableView#XTableView {
color: $primary;
background-color: $background;
}
QTableView#XTableView::item {
background-color: #232323;
font-size: 12px;
}
QTableView#XTableView::item::selected {
background-color: #4aa971;
}
QTableView#XTableView::header {
background-color: #373737;
}

View File

@ -0,0 +1,14 @@
QPlainTextEdit#XTextEdit {
background-color: $dark1;
color: $primary;
border: 2px solid $light3;
}
QPlainTextEdit#XTextEdit:focus {
border: 2px solid $secondaryDark;
}
QPlainTextEdit#XTextEdit:disabled {
color: $light2;
border: 2px solid $dark1;
}

View File

@ -0,0 +1,17 @@
QToolBar#XToolBar {
background-color: #232323;
color: #ffffff;
min-height: 44px;
qproperty-spacing: 32;
}
QToolBar::separator:horizontal {
background: #626262;
width: 1px;
margin: 8px 0;
}
QToolButton {
color: #ffffff;
font-size: 12px;
}

View File

@ -0,0 +1,4 @@
QToolButton#XToolBarItem {
border: none;
outline: none;
}

View File

@ -0,0 +1,54 @@
{
"name": "Xenia",
"description": "The default xenia theme. This theme is dark and clean",
"colors": [
{
"name": "primary",
"value": "#FFFFFF"
},
{
"name": "secondary",
"value": "#5CE494"
},
{
"name": "secondaryDark",
"value": "#4AA971"
},
{
"name": "tertiary",
"value": "#232323"
},
{
"name": "background",
"value": "#1F1F1F"
},
{
"name": "accent",
"value": "#282828"
},
{
"name": "light1",
"value": "#FFFFFF"
},
{
"name": "light2",
"value": "#C7C7C7"
},
{
"name": "light3",
"value": "#7F7F7F"
},
{
"name": "dark1",
"value": "#505050"
},
{
"name": "dark2",
"value": "#373737"
},
{
"name": "dark3",
"value": "#1E1E1E"
}
]
}

View File

@ -0,0 +1,3 @@
#Sidebar {
min-width: 60px;
}

40
src/xenia/app/xenia.qrc Normal file
View File

@ -0,0 +1,40 @@
<RCC>
<qresource prefix="/">
<file>resources/graphics/icon.ico</file>
<file>resources/graphics/GameIconMask.png</file>
<file>resources/graphics/down-arrow.png</file>
<file>resources/graphics/left-arrow.png</file>
<file>resources/graphics/right-arrow.png</file>
<file>resources/graphics/up-arrow.png</file>
<file>resources/fonts/SegMDL2.ttf</file>
<file>resources/fonts/segoeui.ttf</file>
<file>resources/fonts/segoeuisb.ttf</file>
<file>resources/themes/base.css</file>
<file>resources/themes/Xenia/theme.json</file>
<file>resources/themes/Xenia/stylesheets/MainWindow.css</file>
<file>resources/themes/Xenia/stylesheets/XNav.css</file>
<file>resources/themes/Xenia/stylesheets/XTab.css</file>
<file>resources/themes/Xenia/stylesheets/XTabSelector.css</file>
<file>resources/themes/Xenia/stylesheets/XTableView.css</file>
<file>resources/themes/Xenia/stylesheets/XTableHeader.css</file>
<file>resources/themes/Xenia/stylesheets/XSlider.css</file>
<file>resources/themes/Xenia/stylesheets/XToolBar.css</file>
<file>resources/themes/Xenia/stylesheets/XToolBarItem.css</file>
<file>resources/themes/Xenia/stylesheets/HomeTab.css</file>
<file>resources/themes/Xenia/stylesheets/XSideBarButton.css</file>
<file>resources/themes/Xenia/stylesheets/XSideBar.css</file>
<file>resources/themes/Xenia/stylesheets/XStatusBar.css</file>
<file>resources/themes/Xenia/stylesheets/XCard.css</file>
<file>resources/themes/Xenia/stylesheets/XGroupBox.css</file>
<file>resources/themes/Xenia/stylesheets/XPushButton.css</file>
<file>resources/themes/Xenia/stylesheets/XDropdownButton.css</file>
<file>resources/themes/Xenia/stylesheets/XRadioButton.css</file>
<file>resources/themes/Xenia/stylesheets/XCheckBox.css</file>
<file>resources/themes/Xenia/stylesheets/DebugTab.css</file>
<file>resources/themes/Xenia/stylesheets/XComboBox.css</file>
<file>resources/themes/Xenia/stylesheets/XTextEdit.css</file>
<file>resources/themes/Xenia/stylesheets/XLineEdit.css</file>
<file>resources/themes/Xenia/stylesheets/SettingsTab.css</file>
<file>resources/themes/Xenia/stylesheets/XScrollArea.css</file>
</qresource>
</RCC>

View File

@ -2,219 +2,74 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/app/discord/discord_presence.h"
#include "xenia/app/emulator_window.h"
#include "xenia/app/library/game_library.h"
//#include "xenia/base/debugging.h"
//#include "xenia/base/logging.h"
#include "xenia/base/cvar.h"
#include "xenia/base/debugging.h"
#include "xenia/base/logging.h"
#include "xenia/base/main.h"
#include "xenia/base/profiling.h"
#include "xenia/base/threading.h"
//#include "xenia/base/platform.h"
//#include "xenia/base/profiling.h"
//#include "xenia/base/threading.h"
#include "xenia/app/emulator_window.h"
#include "xenia/ui/qt/main_window.h"
#include <QApplication>
#include <QFontDatabase>
#include <QtPlugin>
#include "xenia/base/logging.h"
#include "xenia/config.h"
#include "xenia/debug/ui/debug_window.h"
#include "xenia/emulator.h"
#include "xenia/ui/file_picker.h"
#include "xenia/vfs/devices/host_path_device.h"
#include "xenia/ui/qt/loop_qt.h"
// Available audio systems:
#include "xenia/apu/nop/nop_audio_system.h"
#include "xenia/apu/sdl/sdl_audio_system.h"
#if XE_PLATFORM_WIN32
#include "xenia/apu/xaudio2/xaudio2_audio_system.h"
#endif // XE_PLATFORM_WIN32
#if XE_PLATFORM_WIN32 && QT_STATIC
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
#endif
// Available graphics systems:
#include "xenia/gpu/null/null_graphics_system.h"
#include "xenia/gpu/vulkan/vulkan_graphics_system.h"
#if XE_PLATFORM_WIN32
#include "xenia/gpu/d3d12/d3d12_graphics_system.h"
#endif // XE_PLATFORM_WIN32
// Available input drivers:
#include "xenia/hid/nop/nop_hid.h"
#include "xenia/hid/sdl/sdl_hid.h"
#if XE_PLATFORM_WIN32
#include "xenia/hid/winkey/winkey_hid.h"
#include "xenia/hid/xinput/xinput_hid.h"
#endif // XE_PLATFORM_WIN32
#include "third_party/fmt/include/fmt/format.h"
#include "third_party/xbyak/xbyak/xbyak_util.h"
DEFINE_string(apu, "any", "Audio system. Use: [any, nop, sdl, xaudio2]", "APU");
DEFINE_string(gpu, "any", "Graphics system. Use: [any, d3d12, vulkan, null]",
"GPU");
DEFINE_string(hid, "any", "Input system. Use: [any, nop, sdl, winkey, xinput]",
"HID");
DEFINE_bool(fullscreen, false, "Toggles fullscreen", "GPU");
DEFINE_path(
DEFINE_bool(mount_scratch, false, "Enable scratch mount", "General");
DEFINE_bool(mount_cache, false, "Enable cache mount", "General");
DEFINE_bool(show_debug_tab, true, "Show the debug tab in the Qt UI", "General");
DEFINE_string(
storage_root, "",
"Root path for persistent internal data storage (config, etc.), or empty "
"to use the path preferred for the OS, such as the documents folder, or "
"the emulator executable directory if portable.txt is present in it.",
"Storage");
DEFINE_path(
DEFINE_string(
content_root, "",
"Root path for guest content storage (saves, etc.), or empty to use the "
"content folder under the storage root.",
"Storage");
DEFINE_bool(mount_scratch, false, "Enable scratch mount", "Storage");
DEFINE_bool(mount_cache, false, "Enable cache mount", "Storage");
DEFINE_transient_path(target, "",
"Specifies the target .xex or .iso to execute.",
"General");
DEFINE_transient_bool(portable, false,
"Specifies if Xenia should run in portable mode.",
"General");
DECLARE_bool(debug);
DEFINE_bool(discord, true, "Enable Discord rich presence", "General");
namespace xe {
namespace app {
template <typename T, typename... Args>
class Factory {
private:
struct Creator {
std::string name;
std::function<bool()> is_available;
std::function<std::unique_ptr<T>(Args...)> instantiate;
};
std::vector<Creator> creators_;
public:
void Add(const std::string_view name, std::function<bool()> is_available,
std::function<std::unique_ptr<T>(Args...)> instantiate) {
creators_.push_back({std::string(name), is_available, instantiate});
}
void Add(const std::string_view name,
std::function<std::unique_ptr<T>(Args...)> instantiate) {
auto always_available = []() { return true; };
Add(name, always_available, instantiate);
}
template <typename DT>
void Add(const std::string_view name) {
Add(name, DT::IsAvailable, [](Args... args) {
return std::make_unique<DT>(std::forward<Args>(args)...);
});
}
std::unique_ptr<T> Create(const std::string_view name, Args... args) {
if (!name.empty() && name != "any") {
auto it = std::find_if(
creators_.cbegin(), creators_.cend(),
[&name](const auto& f) { return name.compare(f.name) == 0; });
if (it != creators_.cend() && (*it).is_available()) {
return (*it).instantiate(std::forward<Args>(args)...);
}
return nullptr;
} else {
for (const auto& creator : creators_) {
if (!creator.is_available()) continue;
auto instance = creator.instantiate(std::forward<Args>(args)...);
if (!instance) continue;
return instance;
}
return nullptr;
}
}
std::vector<std::unique_ptr<T>> CreateAll(const std::string_view name,
Args... args) {
std::vector<std::unique_ptr<T>> instances;
if (!name.empty() && name != "any") {
auto it = std::find_if(
creators_.cbegin(), creators_.cend(),
[&name](const auto& f) { return name.compare(f.name) == 0; });
if (it != creators_.cend() && (*it).is_available()) {
auto instance = (*it).instantiate(std::forward<Args>(args)...);
if (instance) {
instances.emplace_back(std::move(instance));
}
}
} else {
for (const auto& creator : creators_) {
if (!creator.is_available()) continue;
auto instance = creator.instantiate(std::forward<Args>(args)...);
if (instance) {
instances.emplace_back(std::move(instance));
}
}
}
return instances;
}
};
std::unique_ptr<apu::AudioSystem> CreateAudioSystem(cpu::Processor* processor) {
Factory<apu::AudioSystem, cpu::Processor*> factory;
#if XE_PLATFORM_WIN32
factory.Add<apu::xaudio2::XAudio2AudioSystem>("xaudio2");
#endif // XE_PLATFORM_WIN32
factory.Add<apu::sdl::SDLAudioSystem>("sdl");
factory.Add<apu::nop::NopAudioSystem>("nop");
return factory.Create(cvars::apu, processor);
}
std::unique_ptr<gpu::GraphicsSystem> CreateGraphicsSystem() {
Factory<gpu::GraphicsSystem> factory;
#if XE_PLATFORM_WIN32
factory.Add<gpu::d3d12::D3D12GraphicsSystem>("d3d12");
#endif // XE_PLATFORM_WIN32
factory.Add<gpu::vulkan::VulkanGraphicsSystem>("vulkan");
factory.Add<gpu::null::NullGraphicsSystem>("null");
return factory.Create(cvars::gpu);
}
std::vector<std::unique_ptr<hid::InputDriver>> CreateInputDrivers(
ui::Window* window) {
std::vector<std::unique_ptr<hid::InputDriver>> drivers;
if (cvars::hid.compare("nop") == 0) {
drivers.emplace_back(xe::hid::nop::Create(window));
} else {
Factory<hid::InputDriver, ui::Window*> factory;
#if XE_PLATFORM_WIN32
factory.Add("xinput", xe::hid::xinput::Create);
// WinKey input driver should always be the last input driver added!
factory.Add("winkey", xe::hid::winkey::Create);
#endif // XE_PLATFORM_WIN32
factory.Add("sdl", xe::hid::sdl::Create);
for (auto& driver : factory.CreateAll(cvars::hid, window)) {
if (XSUCCEEDED(driver->Setup())) {
drivers.emplace_back(std::move(driver));
}
}
if (drivers.empty()) {
// Fallback to nop if none created.
drivers.emplace_back(xe::hid::nop::Create(window));
}
}
return drivers;
}
int xenia_main(const std::vector<std::string>& args) {
Profiler::Initialize();
Profiler::ThreadEnter("main");
/*Profiler::Initialize();
Profiler::ThreadEnter("main");*/
// auto emulator = std::make_unique<xe::Emulator>(L"");
// Start Qt
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
QCoreApplication::setApplicationName("Xenia");
QCoreApplication::setOrganizationName(
"Xenia Xbox 360 Emulator Research Project");
QCoreApplication::setOrganizationDomain("https://xenia.jp");
//QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
//Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
// Figure out where internal files and content should go.
std::filesystem::path storage_root = cvars::storage_root;
if (storage_root.empty()) {
storage_root = xe::filesystem::GetExecutableFolder();
if (!cvars::portable &&
!std::filesystem::exists(storage_root / "portable.txt")) {
if (!std::filesystem::exists(storage_root / "portable.txt")) {
storage_root = xe::filesystem::GetUserFolder();
#if defined(XE_PLATFORM_WIN32) || defined(XE_PLATFORM_LINUX)
storage_root = storage_root / "Xenia";
@ -229,43 +84,28 @@ int xenia_main(const std::vector<std::string>& args) {
config::SetupConfig(storage_root);
std::filesystem::path content_root = cvars::content_root;
if (content_root.empty()) {
content_root = storage_root / "content";
} else {
// If content root isn't an absolute path, then it should be relative to the
// storage root.
if (!content_root.is_absolute()) {
content_root = storage_root / content_root;
}
}
content_root = std::filesystem::absolute(content_root);
XELOGI("Content root: {}", xe::path_to_utf8(content_root));
int argc = 1;
char* argv[] = {"xenia", nullptr};
QApplication app(argc, argv);
if (cvars::discord) {
discord::DiscordPresence::Initialize();
discord::DiscordPresence::NotPlaying();
}
// Load Fonts
QFontDatabase fonts;
fonts.addApplicationFont(":/resources/fonts/SegMDL2.ttf");
fonts.addApplicationFont(":/resources/fonts/segoeui.ttf");
fonts.addApplicationFont(":/resources/fonts/segoeuisb.ttf");
QApplication::setFont(QFont("Segoe UI"));
// Create the emulator but don't initialize so we can setup the window.
auto emulator = std::make_unique<Emulator>("", storage_root, content_root);
// EmulatorWindow main_wnd;
ui::qt::QtLoop loop;
auto main_wnd = new ui::qt::MainWindow(&loop, "Xenia Qt");
main_wnd->Initialize();
main_wnd->SetIcon(QIcon(":/resources/graphics/icon.ico"));
main_wnd->Resize(1280, 720);
// Main emulator display window.
auto emulator_window = EmulatorWindow::Create(emulator.get());
// Setup and initialize all subsystems. If we can't do something
// (unsupported system, memory issues, etc) this will fail early.
X_STATUS result =
emulator->Setup(emulator_window->window(), CreateAudioSystem,
CreateGraphicsSystem, CreateInputDrivers);
if (XFAILED(result)) {
XELOGE("Failed to setup emulator: {:08X}", result);
return 1;
}
if (cvars::mount_scratch) {
/*
if (FLAGS_mount_scratch) {
auto scratch_device = std::make_unique<xe::vfs::HostPathDevice>(
"\\SCRATCH", "scratch", false);
"\\SCRATCH", L"scratch", false);
if (!scratch_device->Initialize()) {
XELOGE("Unable to scan scratch path");
} else {
@ -277,9 +117,13 @@ int xenia_main(const std::vector<std::string>& args) {
}
}
if (cvars::mount_cache) {
if (FLAGS_mount_cache) {
auto cache0_device =
std::make_unique<xe::vfs::HostPathDevice>("\\CACHE0", "cache0", false);
std::make_unique<xe::vfs::HostPathDevice>("\\CACHE0", L"cache0", false);
if (!cache0_device->Initialize()) {
XELOGE("Unable to scan cache0 path");
} else {
@ -291,7 +135,7 @@ int xenia_main(const std::vector<std::string>& args) {
}
auto cache1_device =
std::make_unique<xe::vfs::HostPathDevice>("\\CACHE1", "cache1", false);
std::make_unique<xe::vfs::HostPathDevice>("\\CACHE1", L"cache1", false);
if (!cache1_device->Initialize()) {
XELOGE("Unable to scan cache1 path");
} else {
@ -306,7 +150,7 @@ int xenia_main(const std::vector<std::string>& args) {
// Set a debug handler.
// This will respond to debugging requests so we can open the debug UI.
std::unique_ptr<xe::debug::ui::DebugWindow> debug_window;
if (cvars::debug) {
if (FLAGS_debug) {
emulator->processor()->set_debug_listener_request_handler(
[&](xe::cpu::Processor* processor) {
if (debug_window) {
@ -325,97 +169,19 @@ int xenia_main(const std::vector<std::string>& args) {
return debug_window.get();
});
}
*/
auto evt = xe::threading::Event::CreateAutoResetEvent(false);
emulator->on_launch.AddListener([&](auto title_id, const auto& game_title) {
if (cvars::discord) {
discord::DiscordPresence::PlayingTitle(
game_title.empty() ? "Unknown Title" : std::string(game_title));
}
emulator_window->UpdateTitle();
evt->Set();
});
// if (args.size() >= 2) {
// // Launch the path passed in args[1].
// main_wnd.Launch(args[1]);
//}
emulator->on_shader_storage_initialization.AddListener(
[&](bool initializing) {
emulator_window->SetInitializingShaderStorage(initializing);
});
int rc = app.exec();
loop.AwaitQuit();
emulator->on_terminate.AddListener([&]() {
if (cvars::discord) {
discord::DiscordPresence::NotPlaying();
}
});
emulator_window->window()->on_closing.AddListener([&](ui::UIEvent* e) {
// This needs to shut down before the graphics context.
Profiler::Shutdown();
});
bool exiting = false;
emulator_window->loop()->on_quit.AddListener([&](ui::UIEvent* e) {
exiting = true;
evt->Set();
if (cvars::discord) {
discord::DiscordPresence::Shutdown();
}
// TODO(DrChat): Remove this code and do a proper exit.
XELOGI("Cheap-skate exit!");
exit(0);
});
// Enable the main menu now that the emulator is properly loaded
emulator_window->window()->EnableMainMenu();
// Grab path from the flag or unnamed argument.
std::filesystem::path path;
if (!cvars::target.empty()) {
path = cvars::target;
}
// Toggles fullscreen
if (cvars::fullscreen) emulator_window->ToggleFullscreen();
if (!path.empty()) {
// Normalize the path and make absolute.
auto abs_path = std::filesystem::absolute(path);
result = emulator->LaunchPath(abs_path);
if (XFAILED(result)) {
xe::FatalError(fmt::format("Failed to launch target: {:08X}", result));
emulator.reset();
emulator_window.reset();
return 1;
}
}
// Now, we're going to use the main thread to drive events related to
// emulation.
while (!exiting) {
xe::threading::Wait(evt.get(), false);
while (true) {
emulator->WaitUntilExit();
if (emulator->TitleRequested()) {
emulator->LaunchNextTitle();
} else {
break;
}
}
}
debug_window.reset();
emulator.reset();
if (cvars::discord) {
discord::DiscordPresence::Shutdown();
}
Profiler::Dump();
Profiler::Shutdown();
emulator_window.reset();
return 0;
/*Profiler::Dump();
Profiler::Shutdown();*/
return rc;
}
} // namespace app

View File

@ -128,7 +128,7 @@ int Main() {
// Setup COM on the main thread.
// NOTE: this may fail if COM has already been initialized - that's OK.
#pragma warning(suppress : 6031)
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
// Initialize logging. Needs parsed FLAGS.
xe::InitializeLogging(entry_info.name);

View File

@ -90,15 +90,13 @@ bool DebugWindow::Initialize() {
loop_->on_quit.AddListener([this](UIEvent* e) { window_.reset(); });
// Main menu.
auto main_menu = MenuItem::Create(MenuItem::Type::kNormal);
auto file_menu = MenuItem::Create(MenuItem::Type::kPopup, "&File");
auto main_menu = window_->CreateMenu();
auto file_menu = main_menu->CreateMenuItem("&File");
{
file_menu->AddChild(MenuItem::Create(MenuItem::Type::kString, "&Close",
"Alt+F4",
[this]() { window_->Close(); }));
file_menu->CreateChild(MenuItem::Type::kString, "&Close", "Alt+F4",
[this]() { window_->Close(); });
}
main_menu->AddChild(std::move(file_menu));
window_->set_main_menu(std::move(main_menu));
window_->Resize(1500, 1000);

View File

@ -19,7 +19,7 @@
#include "xenia/cpu/processor.h"
#include "xenia/emulator.h"
#include "xenia/ui/loop.h"
#include "xenia/ui/menu_item.h"
#include "xenia/ui/menu.h"
#include "xenia/ui/window.h"
#include "xenia/xbox.h"

View File

@ -79,7 +79,8 @@ Emulator::Emulator(const std::filesystem::path& command_line,
title_id_(0),
paused_(false),
restoring_(false),
restore_fence_() {}
restore_fence_(),
title_data_(nullptr, 0) {}
Emulator::~Emulator() {
// Note that we delete things in the reverse order they were initialized.
@ -106,8 +107,7 @@ Emulator::~Emulator() {
ExceptionHandler::Uninstall(Emulator::ExceptionCallbackThunk, this);
}
X_STATUS Emulator::Setup(
ui::Window* display_window,
X_STATUS Emulator::Setup(ui::Window* display_window,
std::function<std::unique_ptr<apu::AudioSystem>(cpu::Processor*)>
audio_system_factory,
std::function<std::unique_ptr<gpu::GraphicsSystem>()>
@ -162,6 +162,12 @@ X_STATUS Emulator::Setup(
return X_STATUS_UNSUCCESSFUL;
}
// Bring up the virtual filesystem used by the kernel.
file_system_ = std::make_unique<xe::vfs::VirtualFileSystem>();
// Shared kernel state.
kernel_state_ = std::make_unique<xe::kernel::KernelState>(this);
// Initialize the APU.
if (audio_system_factory) {
audio_system_ = audio_system_factory(processor_.get());
@ -193,45 +199,14 @@ X_STATUS Emulator::Setup(
return result;
}
// Bring up the virtual filesystem used by the kernel.
file_system_ = std::make_unique<xe::vfs::VirtualFileSystem>();
// Shared kernel state.
kernel_state_ = std::make_unique<xe::kernel::KernelState>(this);
// Setup the core components.
result = graphics_system_->Setup(processor_.get(), kernel_state_.get(),
display_window_);
if (result) {
return result;
}
if (audio_system_) {
result = audio_system_->Setup(kernel_state_.get());
if (result) {
return result;
}
}
#define LOAD_KERNEL_MODULE(t) \
static_cast<void>(kernel_state_->LoadKernelModule<kernel::t>())
// HLE kernel modules.
LOAD_KERNEL_MODULE(xboxkrnl::XboxkrnlModule);
LOAD_KERNEL_MODULE(xam::XamModule);
LOAD_KERNEL_MODULE(xbdm::XbdmModule);
#undef LOAD_KERNEL_MODULE
kernel_state_->LoadKernelModule<kernel::xboxkrnl::XboxkrnlModule>();
kernel_state_->LoadKernelModule<kernel::xam::XamModule>();
kernel_state_->LoadKernelModule<kernel::xbdm::XbdmModule>();
// Initialize emulator fallback exception handling last.
ExceptionHandler::Install(Emulator::ExceptionCallbackThunk, this);
if (display_window_) {
// Finish initializing the display.
display_window_->loop()->PostSynchronous([this]() {
xe::ui::GraphicsContextLock context_lock(display_window_->context());
Profiler::set_window(display_window_);
});
}
return result;
}
@ -481,7 +456,7 @@ bool Emulator::RestoreFromFile(const std::filesystem::path& path) {
kernel_state_->object_table()->GetObjectsByType<kernel::XThread>();
for (auto thread : threads) {
if (thread->main_thread()) {
main_thread_ = thread;
main_thread_ = thread->thread();
break;
}
}
@ -584,9 +559,7 @@ bool Emulator::ExceptionCallback(Exception* ex) {
void Emulator::WaitUntilExit() {
while (true) {
if (main_thread_) {
xe::threading::Wait(main_thread_->thread(), false);
}
xe::threading::Wait(main_thread_, false);
if (restoring_) {
restore_fence_.Wait();
@ -688,12 +661,12 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path,
on_shader_storage_initialization(true);
graphics_system_->InitializeShaderStorage(storage_root_, title_id_, true);
on_shader_storage_initialization(false);
auto main_thread = kernel_state_->LaunchModule(module);
if (!main_thread) {
auto main_xthread = kernel_state_->LaunchModule(module);
if (!main_xthread) {
return X_STATUS_UNSUCCESSFUL;
}
main_thread_ = main_thread;
main_thread_ = main_xthread->thread();
on_launch(title_id_, game_title_);
return X_STATUS_SUCCESS;

View File

@ -16,6 +16,7 @@
#include "xenia/base/delegate.h"
#include "xenia/base/exception_handler.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/util/xdbf_utils.h"
#include "xenia/memory.h"
#include "xenia/vfs/virtual_file_system.h"
#include "xenia/xbox.h"
@ -103,13 +104,21 @@ class Emulator {
// This is effectively the guest operating system.
kernel::KernelState* kernel_state() const { return kernel_state_.get(); }
// Get the database with information about the running game.
const kernel::util::XdbfGameData* game_data() const {
if (title_data_.is_valid()) {
return &title_data_;
}
return nullptr;
}
// Initializes the emulator and configures all components.
// The given window is used for display and the provided functions are used
// to create subsystems as required.
// Once this function returns a game can be launched using one of the Launch
// functions.
X_STATUS Setup(
ui::Window* display_window,
X_STATUS Setup(ui::Window* display_window,
std::function<std::unique_ptr<apu::AudioSystem>(cpu::Processor*)>
audio_system_factory,
std::function<std::unique_ptr<gpu::GraphicsSystem>()>
@ -182,8 +191,9 @@ class Emulator {
std::unique_ptr<vfs::VirtualFileSystem> file_system_;
std::unique_ptr<kernel::KernelState> kernel_state_;
kernel::object_ref<kernel::XThread> main_thread_;
uint32_t title_id_; // Currently running title ID
threading::Thread* main_thread_ = nullptr;
kernel::util::XdbfGameData title_data_; // Currently running title DB
uint32_t title_id_ = 0; // Currently running title ID
bool paused_;
bool restoring_;

View File

@ -473,6 +473,11 @@ struct xex2_opt_execution_info {
};
static_assert_size(xex2_opt_execution_info, 0x18);
struct xex2_multi_disc_media_id_t {
char hash[12];
uint32_t media_id;
};
struct xex2_opt_import_libraries {
xe::be<uint32_t> size; // 0x0
struct {

View File

@ -287,7 +287,7 @@ void ImGuiDrawer::OnMouseDown(MouseEvent* e) {
if (button >= 0 && button < std::size(io.MouseDown)) {
if (!ImGui::IsAnyMouseDown()) {
window_->CaptureMouse();
//window_->CaptureMouse();
}
io.MouseDown[button] = true;
}
@ -320,7 +320,7 @@ void ImGuiDrawer::OnMouseUp(MouseEvent* e) {
if (button >= 0 && button < std::size(io.MouseDown)) {
io.MouseDown[button] = false;
if (!ImGui::IsAnyMouseDown()) {
window_->ReleaseMouse();
//window_->ReleaseMouse();
}
}
}

View File

@ -7,27 +7,16 @@
******************************************************************************
*/
#include "xenia/ui/menu_item.h"
#include "xenia/ui/menu.h"
namespace xe {
namespace ui {
std::unique_ptr<MenuItem> MenuItem::Create(Type type) {
return MenuItem::Create(type, "", "", nullptr);
}
std::unique_ptr<MenuItem> MenuItem::Create(Type type, const std::string& text) {
return MenuItem::Create(type, text, "", nullptr);
}
std::unique_ptr<MenuItem> MenuItem::Create(Type type, const std::string& text,
std::function<void()> callback) {
return MenuItem::Create(type, text, "", std::move(callback));
}
MenuItem::MenuItem(Type type, const std::string& text,
MenuItem::MenuItem(Menu* menu, Type type, const std::string& text,
const std::string& hotkey, std::function<void()> callback)
: type_(type),
: menu_(menu),
type_(type),
parent_item_(nullptr),
text_(text),
hotkey_(hotkey),
@ -35,21 +24,6 @@ MenuItem::MenuItem(Type type, const std::string& text,
MenuItem::~MenuItem() = default;
void MenuItem::AddChild(MenuItem* child_item) {
AddChild(MenuItemPtr(child_item, [](MenuItem* item) {}));
}
void MenuItem::AddChild(std::unique_ptr<MenuItem> child_item) {
AddChild(
MenuItemPtr(child_item.release(), [](MenuItem* item) { delete item; }));
}
void MenuItem::AddChild(MenuItemPtr child_item) {
auto child_item_ptr = child_item.get();
children_.emplace_back(std::move(child_item));
OnChildAdded(child_item_ptr);
}
void MenuItem::RemoveChild(MenuItem* child_item) {
for (auto it = children_.begin(); it != children_.end(); ++it) {
if (it->get() == child_item) {

View File

@ -7,8 +7,8 @@
******************************************************************************
*/
#ifndef XENIA_UI_MENU_ITEM_H_
#define XENIA_UI_MENU_ITEM_H_
#ifndef XENIA_UI_MENU_H_
#define XENIA_UI_MENU_H_
#include <functional>
#include <memory>
@ -23,24 +23,17 @@ namespace ui {
class Window;
class MenuItem {
public:
typedef std::unique_ptr<MenuItem, void (*)(MenuItem*)> MenuItemPtr;
friend class Menu;
public:
using Callback = std::function<void()>;
enum class Type {
kPopup, // Popup menu (submenu)
kSubmenu, // Submenu (Popup menu in win32)
kSeparator,
kNormal, // Root menu
kRoot, // Root menu
kString, // Menu is just a string
};
static std::unique_ptr<MenuItem> Create(Type type);
static std::unique_ptr<MenuItem> Create(Type type, const std::string& text);
static std::unique_ptr<MenuItem> Create(Type type, const std::string& text,
std::function<void()> callback);
static std::unique_ptr<MenuItem> Create(Type type, const std::string& text,
const std::string& hotkey,
std::function<void()> callback);
virtual ~MenuItem();
MenuItem* parent_item() const { return parent_item_; }
@ -48,9 +41,9 @@ class MenuItem {
const std::string& text() { return text_; }
const std::string& hotkey() { return hotkey_; }
void AddChild(MenuItem* child_item);
void AddChild(std::unique_ptr<MenuItem> child_item);
void AddChild(MenuItemPtr child_item);
virtual MenuItem* CreateChild(Type type, std::string text = "",
std::string hotkey = "",
Callback callback = nullptr) = 0;
void RemoveChild(MenuItem* child_item);
MenuItem* child(size_t index);
@ -58,22 +51,57 @@ class MenuItem {
virtual void DisableMenuItem(Window& window) = 0;
protected:
MenuItem(Type type, const std::string& text, const std::string& hotkey,
std::function<void()> callback);
MenuItem(Menu* menu, Type type, const std::string& text,
const std::string& hotkey, Callback callback);
virtual void OnChildAdded(MenuItem* child_item) {}
virtual void OnChildRemoved(MenuItem* child_item) {}
virtual void OnSelected(UIEvent* e);
Menu* menu_;
Type type_;
MenuItem* parent_item_;
std::vector<MenuItemPtr> children_;
std::vector<std::unique_ptr<MenuItem>> children_;
std::string text_;
std::string hotkey_;
std::function<void()> callback_;
};
class Menu {
using MenuType = MenuItem::Type;
public:
explicit Menu(Window* window) : window_(window), enabled_(true) {}
virtual ~Menu() = default;
virtual MenuItem* CreateMenuItem(const std::string& title) = 0;
Window* window() const { return window_; }
const std::vector<std::unique_ptr<MenuItem>>& menu_items() const {
return menu_items_;
}
bool enabled() const { return enabled_; }
void set_enabled(bool enabled) {
enabled_ = enabled;
for (const auto& menu_item : menu_items_) {
if (enabled) {
menu_item->EnableMenuItem(*window());
} else {
menu_item->DisableMenuItem(*window());
}
}
}
protected:
Window* window_;
std::vector<std::unique_ptr<MenuItem>> menu_items_;
bool enabled_;
};
} // namespace ui
} // namespace xe

124
src/xenia/ui/menu_win.cc Normal file
View File

@ -0,0 +1,124 @@
/**
******************************************************************************
* 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 "menu_win.h"
#include "window.h"
namespace xe {
namespace ui {
MenuItem* Win32MenuItem::CreateChild(Type type, std::string text,
std::string hotkey, Callback callback) {
auto child = std::unique_ptr<Win32MenuItem>(new Win32MenuItem(
static_cast<Win32Menu*>(menu_), type, text, hotkey, callback));
children_.push_back(std::move(child));
auto child_ptr = children_.back().get();
OnChildAdded(child_ptr);
return child_ptr;
}
Win32MenuItem::Win32MenuItem(Win32Menu* menu, Type type,
const std::string& text,
const std::string& hotkey,
std::function<void()> callback)
: MenuItem(menu, type, text, hotkey, std::move(callback)),
handle_(nullptr) {
switch (type) {
case Type::kRoot:
case Type::kSubmenu: {
handle_ = CreatePopupMenu();
break;
}
default:
// May just be a placeholder.
break;
}
if (handle_) {
MENUINFO menu_info = {0};
menu_info.cbSize = sizeof(menu_info);
menu_info.fMask = MIM_MENUDATA | MIM_STYLE;
menu_info.dwMenuData = ULONG_PTR(this);
menu_info.dwStyle = MNS_NOTIFYBYPOS;
SetMenuInfo(handle_, &menu_info);
}
}
void Win32MenuItem::EnableMenuItem(Window& window) {
int i = 0;
for (auto iter = children_.begin(); iter != children_.end(); ++iter, i++) {
::EnableMenuItem(handle_, i, MF_BYPOSITION | MF_ENABLED);
}
DrawMenuBar((HWND)window.native_handle());
}
void Win32MenuItem::DisableMenuItem(Window& window) {
int i = 0;
for (auto iter = children_.begin(); iter != children_.end(); ++iter, i++) {
::EnableMenuItem(handle_, i, MF_BYPOSITION | MF_GRAYED);
}
DrawMenuBar((HWND)window.native_handle());
}
void Win32MenuItem::OnChildAdded(MenuItem* generic_child_item) {
auto child_item = static_cast<Win32MenuItem*>(generic_child_item);
switch (child_item->type()) {
case MenuItem::Type::kRoot:
// Nothing special.
break;
case MenuItem::Type::kSubmenu:
AppendMenuW(handle_, MF_POPUP,
reinterpret_cast<UINT_PTR>(child_item->handle()),
reinterpret_cast<LPCWSTR>(xe::to_utf16(child_item->text()).c_str()));
break;
case MenuItem::Type::kSeparator:
AppendMenuW(handle_, MF_SEPARATOR, UINT_PTR(child_item->handle()), 0);
break;
case MenuItem::Type::kString:
auto full_name = child_item->text();
if (!child_item->hotkey().empty()) {
full_name += "\t" + child_item->hotkey();
}
AppendMenuW(handle_, MF_STRING, UINT_PTR(child_item->handle_),
reinterpret_cast<LPCWSTR>(xe::to_utf16(full_name).c_str()));
break;
}
}
void Win32MenuItem::OnChildRemoved(MenuItem* generic_child_item) {}
Win32Menu::Win32Menu(Window* window) : Menu(window) {
menu_ = ::CreateMenu();
MENUINFO menu_info = {0};
menu_info.cbSize = sizeof(menu_info);
menu_info.fMask = MIM_MENUDATA | MIM_STYLE;
menu_info.dwMenuData = ULONG_PTR(this);
menu_info.dwStyle = MNS_NOTIFYBYPOS;
SetMenuInfo(menu_, &menu_info);
}
Win32Menu::~Win32Menu() { DestroyMenu(menu_); }
MenuItem* Win32Menu::CreateMenuItem(const std::string& title) {
auto menu_item = std::unique_ptr<Win32MenuItem>(
new Win32MenuItem(this, MenuType::kRoot, title, "", nullptr));
auto menu_item_ptr = menu_item.get();
menu_items_.push_back(std::move(menu_item));
AppendMenuW(menu_, MF_POPUP,
reinterpret_cast<UINT_PTR>(menu_item_ptr->handle()),
reinterpret_cast<LPCWSTR>(menu_item_ptr->text().c_str()));
return menu_item_ptr;
}
} // namespace ui
} // namespace xe

59
src/xenia/ui/menu_win.h Normal file
View File

@ -0,0 +1,59 @@
/**
******************************************************************************
* 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. *
******************************************************************************
*/
#ifndef XENIA_UI_MENU_WIN_H_
#define XENIA_UI_MENU_WIN_H_
#include "menu.h"
#include "xenia/base/platform_win.h"
namespace xe {
namespace ui {
class Win32MenuItem : public MenuItem {
friend class Win32Menu;
public:
MenuItem* CreateChild(Type type, std::string text, std::string hotkey,
Callback callback) override;
HMENU handle() const { return handle_; }
void EnableMenuItem(Window& window) override;
void DisableMenuItem(Window& window) override;
using MenuItem::OnSelected;
protected:
void OnChildAdded(MenuItem* child_item) override;
void OnChildRemoved(MenuItem* child_item) override;
Win32MenuItem(Win32Menu *menu, Type type, const std::string& text, const std::string& hotkey,
std::function<void()> callback);
private:
// menu item handle (if submenu)
HMENU handle_;
};
class Win32Menu : public Menu {
public:
explicit Win32Menu(Window* window);
~Win32Menu();
HMENU handle() const { return menu_; }
MenuItem* CreateMenuItem(const std::string& title) override;
private:
HMENU menu_;
};
} // namespace ui
} // namespace xe
#endif

View File

@ -0,0 +1,65 @@
#include "xenia/ui/qt/actions/action.h"
#include <QPainter>
#include "xenia/ui/qt/theme_manager.h"
namespace xe {
namespace ui {
namespace qt {
XAction::XAction() {}
XAction::XAction(const QChar& icon, const QString& text) {
setGlyphIcon(QFont("Segoe MDL2 Assets", 64), icon);
setIconText(text);
}
void XAction::setGlyphIcon(const QFont& font, const QChar& glyph_char) {
glyph_char_ = glyph_char;
glyph_font_ = font;
rebuildGlyphIcons();
}
void XAction::rebuildGlyphIcons() {
auto renderPixmap = [=](QColor color) {
// Measure the Glyph
QFontMetrics measure(glyph_font_);
QRect icon_rect = measure.boundingRect(glyph_char_);
double max = qMax(icon_rect.width(), icon_rect.height());
// Create the Pixmap
// boundingRect can be inaccurate so add a 4px padding to be safe
QPixmap pixmap(max + 4, max + 4);
pixmap.fill(Qt::transparent);
// Paint the Glyph
QPainter painter(&pixmap);
painter.setFont(glyph_font_);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
painter.setPen(QPen(color));
painter.drawText(pixmap.rect(), Qt::AlignVCenter, glyph_char_);
return pixmap;
};
auto theme_manager = ThemeManager::Instance();
const Theme& current_theme = theme_manager.current_theme();
QColor off_color = current_theme.ColorForKey("light2");
QColor on_color = current_theme.ColorForKey("secondary");
QPixmap OFF = renderPixmap(off_color);
QPixmap ON = renderPixmap(on_color);
QIcon icon;
icon.addPixmap(OFF, QIcon::Normal, QIcon::Off);
icon.addPixmap(ON, QIcon::Normal, QIcon::On);
setIcon(icon);
}
} // namespace qt
} // namespace ui
} // namespace xe

View File

@ -0,0 +1,35 @@
#ifndef XENIA_UI_QT_ACTION_H_
#define XENIA_UI_QT_ACTION_H_
#include "xenia/ui/qt/themeable_widget.h"
#include <QAction>
#include <QPixmap>
namespace xe {
namespace ui {
namespace qt {
class XAction : public QAction {
Q_OBJECT
public:
explicit XAction();
explicit XAction(const QChar& icon, const QString& text);
void setGlyphIcon(const QFont& font, const QChar& icon);
private:
void rebuildGlyphIcons();
// Glyph Icon
QChar glyph_char_;
QPalette glyph_palette_;
QFont glyph_font_;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

View File

View File

@ -0,0 +1,24 @@
#ifndef XENIA_UI_QT_ADD_GAME_ACTION_H_
#define XENIA_UI_QT_ADD_GAME_ACTION_H_
#include "xenia/ui/qt/actions/action.h"
namespace xe {
namespace ui {
namespace qt {
class XAddGameAction : public XAction {
Q_OBJECT
public:
explicit XAddGameAction() : XAction() {
setIconText("Add Game");
setGlyphIcon(QFont("Segoe MDL2 Assets"), QChar(0xE710));
}
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

View File

@ -0,0 +1,91 @@
#include "xenia/ui/qt/delegates/game_listview_delegate.h"
#include "xenia/ui/qt/models/game_library_model.h"
#include <QBitmap>
#include <QImage>
#include <QPainter>
namespace xe {
namespace ui {
namespace qt {
XGameListViewDelegate::XGameListViewDelegate(QWidget* parent)
: QStyledItemDelegate(parent) {
QImage mask_image(":resources/graphics/GameIconMask.png");
icon_mask_ = QPixmap::fromImage(mask_image.createAlphaMask());
}
void XGameListViewDelegate::paint(QPainter* painter,
const QStyleOptionViewItem& option,
const QModelIndex& index) const {
painter->save();
auto options = QStyleOptionViewItem(option);
options.state &= (~QStyle::State_HasFocus);
initStyleOption(&options, index);
GameColumn column = (GameColumn)index.column();
switch (column) {
case GameColumn::kIconColumn: {
if (index.data().canConvert<QImage>()) {
QStyledItemDelegate::paint(painter, options, index);
QImage image = qvariant_cast<QImage>(index.data());
QPixmap pixmap = QPixmap::fromImage(image);
pixmap.setDevicePixelRatio(painter->device()->devicePixelRatioF());
paintIcon(pixmap, painter, options, index);
}
break;
}
default: {
QStyledItemDelegate::paint(painter, options, index);
}
}
painter->restore();
}
void XGameListViewDelegate::paintIcon(QPixmap& icon, QPainter* painter,
const QStyleOptionViewItem& options,
const QModelIndex& index) const {
painter->save();
// Get the column bounds
double col_x = options.rect.x();
double col_y = options.rect.y();
double col_width = options.rect.width();
double col_height = options.rect.height();
double icon_size = options.rect.height() * 0.8;
icon.setMask(icon_mask_);
// Calculate the Icon position
QRectF rect = icon.rect();
QRectF icon_rect = QRectF(rect.x(), rect.y(), icon_size, icon_size);
double shift_x = (col_width - icon_size) / 2 + col_x;
double shift_y = (col_height - icon_size) / 2 + col_y;
icon_rect.translate(shift_x, shift_y);
// adding QPainter::Antialiasing here smoothes masked edges
// but makes the image look slightly blurry
painter->setRenderHints(QPainter::SmoothPixmapTransform);
painter->drawPixmap(icon_rect, icon, icon.rect());
painter->restore();
}
QSize XGameListViewDelegate::sizeHint(const QStyleOptionViewItem& option,
const QModelIndex& index) const {
GameColumn column = (GameColumn)index.column();
switch (column) {
case GameColumn::kIconColumn:
return QSize(58, 48);
case GameColumn::kPathColumn: {
return QSize(500, 48);
}
default:
return QSize(96, 48);
}
}
} // namespace qt
} // namespace ui
} // namespace xe

View File

@ -0,0 +1,34 @@
#ifndef XENIA_UI_QT_GAME_LISTVIEW_DELEGATE_H_
#define XENIA_UI_QT_GAME_LISTVIEW_DELEGATE_H_
#include <QPixmap>
#include <QStyledItemDelegate>
namespace xe {
namespace ui {
namespace qt {
class XGameListViewDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
explicit XGameListViewDelegate(QWidget* parent = nullptr);
void paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const override;
QSize sizeHint(const QStyleOptionViewItem& options,
const QModelIndex& index) const override;
private:
void paintIcon(QPixmap& icon, QPainter* painter,
const QStyleOptionViewItem& options,
const QModelIndex& index) const;
QPixmap icon_mask_;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

View File

@ -0,0 +1,68 @@
/**
******************************************************************************
* 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 "loop_qt.h"
#include <QApplication>
#include <QThread>
#include <QTimer>
#include "xenia/base/assert.h"
#include "xenia/base/threading.h"
namespace xe {
namespace ui {
std::unique_ptr<Loop> Loop::Create() { return std::make_unique<qt::QtLoop>(); }
namespace qt {
bool QtLoop::is_on_loop_thread() {
return QThread::currentThread() == QApplication::instance()->thread();
}
QtLoop::QtLoop() : has_quit_(false) {
QObject::connect(qApp, &QCoreApplication::aboutToQuit,
[this]() { has_quit_ = true; });
}
void QtLoop::Post(std::function<void()> fn) { PostDelayed(fn, 0); }
void QtLoop::PostDelayed(std::function<void()> fn, uint64_t delay_millis) {
// https://riptutorial.com/qt/example/21783/using-qtimer-to-run-code-on-main-thread
const auto& main_thread = QApplication::instance()->thread();
QTimer* timer = new QTimer();
timer->moveToThread(main_thread);
timer->setSingleShot(true);
QObject::connect(timer, &QTimer::timeout, [=]() {
// we are now in the gui thread
fn();
timer->deleteLater();
});
QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection,
Q_ARG(uint64_t, delay_millis));
}
void QtLoop::Quit() {
QMetaObject::invokeMethod(qApp, "quit", Qt::QueuedConnection);
}
void QtLoop::AwaitQuit() {
if (has_quit_) return;
xe::threading::Fence fence;
QObject::connect(qApp, &QCoreApplication::aboutToQuit,
[&fence]() { fence.Signal(); });
fence.Wait();
}
} // namespace qt
} // namespace ui
} // namespace xe

38
src/xenia/ui/qt/loop_qt.h Normal file
View File

@ -0,0 +1,38 @@
/**
******************************************************************************
* 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.h"
#ifndef XENIA_UI_QT_LOOP_QT_H_
#define XENIA_UI_QT_LOOP_QT_H_
namespace xe {
namespace ui {
namespace qt {
class QtLoop final : public Loop {
public:
QtLoop();
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:
bool has_quit_;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

View File

@ -0,0 +1,50 @@
#include "xenia/ui/qt/main_window.h"
#include <QVBoxLayout>
#include "build/version.h"
#include "xenia/ui/qt/widgets/status_bar.h"
namespace xe {
namespace ui {
namespace qt {
bool MainWindow::Initialize() {
// Custom Frame Border
// Disable for now until windows aero additions are added
// setWindowFlags(Qt::Window | Qt::FramelessWindowHint);
this->setFocusPolicy(Qt::StrongFocus);
shell_ = new XShell(this);
this->setCentralWidget(shell_);
status_bar_ = new XStatusBar(this);
this->setStatusBar(status_bar_);
QLabel* build_label = new QLabel;
build_label->setObjectName("buildLabel");
build_label->setText(QStringLiteral("Xenia: %1 / %2 / %3")
.arg(XE_BUILD_BRANCH)
.arg(XE_BUILD_COMMIT_SHORT)
.arg(XE_BUILD_DATE));
status_bar_->addPermanentWidget(build_label);
return QtWindow::Initialize();
}
void MainWindow::AddStatusBarWidget(QWidget* widget, bool permanent) {
if (permanent) {
status_bar_->addPermanentWidget(widget);
} else {
status_bar_->addWidget(widget);
}
}
void MainWindow::RemoveStatusBarWidget(QWidget* widget) {
return status_bar_->removeWidget(widget);
}
} // namespace qt
} // namespace ui
} // namespace xe

View File

@ -0,0 +1,37 @@
#ifndef XENIA_UI_QT_MAINWINDOW_H_
#define XENIA_UI_QT_MAINWINDOW_H_
#include <QMainWindow>
#include "window_qt.h"
#include "xenia/ui/qt/themeable_widget.h"
#include "xenia/ui/qt/widgets/shell.h"
namespace xe {
namespace ui {
namespace qt {
class XStatusBar;
class MainWindow final : public Themeable<QtWindow> {
Q_OBJECT
public:
MainWindow(Loop* loop, const std::string& title)
: Themeable<QtWindow>("MainWindow", loop, title) {}
bool Initialize() override;
void AddStatusBarWidget(QWidget* widget, bool permanent = false);
void RemoveStatusBarWidget(QWidget* widget);
const XStatusBar* status_bar() const { return status_bar_; }
private:
XShell* shell_ = nullptr;
XStatusBar* status_bar_ = nullptr;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

112
src/xenia/ui/qt/menu_qt.cc Normal file
View File

@ -0,0 +1,112 @@
/**
******************************************************************************
* 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 "menu_qt.h"
#include "window_qt.h"
namespace xe {
namespace ui {
namespace qt {
MenuItem* QtMenuItem::CreateChild(Type type, std::string text,
std::string hotkey, Callback callback) {
auto child = std::unique_ptr<QtMenuItem>(new QtMenuItem(
static_cast<QtMenu*>(menu_), type, text, hotkey, callback));
children_.push_back(std::move(child));
auto child_ptr = children_.back().get();
OnChildAdded(child_ptr);
return child_ptr;
}
void QtMenuItem::EnableMenuItem(Window& window) {
qt_menu_->setEnabled(true);
for (const auto& child : children_) {
child->EnableMenuItem(window);
}
}
void QtMenuItem::DisableMenuItem(Window& window) {
qt_menu_->setDisabled(true);
for (const auto& child : children_) {
child->DisableMenuItem(window);
}
}
QtMenuItem::QtMenuItem(QtMenu* menu, Type type, const std::string& text,
const std::string& hotkey,
std::function<void()> callback)
: MenuItem(menu, type, text, hotkey, callback), qt_menu_(nullptr) {
}
void QtMenuItem::OnChildAdded(MenuItem* generic_child_item) {
assert(qt_menu_ != nullptr);
auto child_item = static_cast<QtMenuItem*>(generic_child_item);
switch (child_item->type()) {
case Type::kSubmenu: {
child_item->qt_menu_ =
new QMenu(QString::fromStdString(child_item->text()));
qt_menu_->addMenu(child_item->qt_menu_);
} break;
case Type::kSeparator: {
qt_menu_->addSeparator();
} break;
case Type::kString: {
auto title = QString::fromStdString(child_item->text());
auto hotkey = child_item->hotkey();
auto shortcut =
hotkey.empty()
? 0
: QKeySequence(QString::fromStdString(hotkey));
const auto& callback = child_item->callback_;
qt_menu_->addAction(
title,
[&callback]() {
if (callback) {
callback();
}
},
shortcut);
} break;
case Type::kRoot:
default:
break;
}
}
void QtMenuItem::OnChildRemoved(MenuItem* child_item) {}
QtMenu::QtMenu(Window* window) : Menu(window) {
auto qt_window = static_cast<QtWindow*>(window);
menu_bar_ = qt_window->menuBar();
}
MenuItem* QtMenu::CreateMenuItem(const std::string& title) {
auto menu_item = std::unique_ptr<QtMenuItem>(
new QtMenuItem(this, MenuType::kRoot, title, "", nullptr));
auto menu = new QMenu(QString::fromStdString(title));
menu_item->qt_menu_ = menu;
menu_items_.push_back(std::move(menu_item));
menu_bar_->addMenu(menu);
return menu_items_.back().get();
}
} // namespace qt
} // namespace ui
} // namespace xe

55
src/xenia/ui/qt/menu_qt.h Normal file
View File

@ -0,0 +1,55 @@
/**
******************************************************************************
* 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. *
******************************************************************************
*/
#ifndef XENIA_UI_QT_MENU_QT_H_
#define XENIA_UI_QT_MENU_QT_H_
#include <QMenu>
#include <QMenuBar>
#include "xenia/ui/menu.h"
namespace xe {
namespace ui {
namespace qt {
class QtMenuItem : public MenuItem {
friend class QtMenu;
public:
MenuItem* CreateChild(Type type, std::string text, std::string hotkey,
Callback callback) override;
void EnableMenuItem(Window& window) override;
void DisableMenuItem(Window& window) override;
protected:
QtMenuItem(QtMenu* menu, Type type, const std::string& text,
const std::string& hotkey, std::function<void()> callback);
void OnChildAdded(MenuItem* child_item) override;
void OnChildRemoved(MenuItem* child_item) override;
private:
QMenu* qt_menu_;
};
class QtMenu : public Menu {
public:
explicit QtMenu(Window* window);
MenuItem* CreateMenuItem(const std::string& title) override;
QMenuBar* menu_bar() const { return menu_bar_; }
private:
QMenuBar* menu_bar_;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

View File

@ -0,0 +1,171 @@
#include "xenia/ui/qt/models/game_library_model.h"
#include "xenia/base/string_util.h"
#include <QIcon>
#include <QLabel>
namespace xe {
namespace ui {
namespace qt {
XGameLibraryModel::XGameLibraryModel(QObject* parent) {
library_ = XGameLibrary::Instance();
}
QVariant XGameLibraryModel::data(const QModelIndex& index, int role) const {
if (!index.isValid()) {
return QVariant();
}
const XGameEntry& entry = library_->games()[index.row()];
GameColumn column = (GameColumn)index.column();
switch (column) {
case GameColumn::kIconColumn:
if (role == Qt::DisplayRole) {
QImage image;
image.loadFromData(entry.icon(), (int)entry.icon_size());
return image;
}
break;
case GameColumn::kTitleColumn:
if (role == Qt::DisplayRole) {
return QString::fromUtf8(entry.title().c_str());
}
break;
case GameColumn::kTitleIdColumn:
if (role == Qt::DisplayRole) {
auto title_id = xe::string_util::to_hex_string(entry.title_id());
return QString::fromUtf8(title_id.c_str());
}
break;
case GameColumn::kMediaIdColumn:
if (role == Qt::DisplayRole) {
auto media_id = xe::string_util::to_hex_string(entry.media_id());
return QString::fromUtf8(media_id.c_str());
}
break;
case GameColumn::kPathColumn:
if (role == Qt::DisplayRole) {
return QString::fromWCharArray(entry.file_path().c_str());
}
break;
case GameColumn::kVersionColumn:
if (role == Qt::DisplayRole) {
auto version = &entry.version();
QString version_str;
version_str.asprintf("v%u.%u.%u.%u", version->major, version->minor,
version->build, version->qfe);
return version_str;
}
case GameColumn::kGenreColumn:
if (role == Qt::DisplayRole) {
return QString::fromUtf8(entry.genre().c_str());
}
break;
case GameColumn::kReleaseDateColumn:
if (role == Qt::DisplayRole) {
return QString::fromUtf8(entry.release_date().c_str());
}
break;
case GameColumn::kBuildDateColumn:
if (role == Qt::DisplayRole) {
return QString::fromUtf8(entry.build_date().c_str());
}
break;
case GameColumn::kLastPlayedColumn:
return QVariant(); // TODO
case GameColumn::kTimePlayedColumn:
return QVariant(); // TODO
case GameColumn::kAchievementsUnlockedColumn:
return QVariant(); // TODO
case GameColumn::kGamerscoreUnlockedColumn:
return QVariant(); // TODO
case GameColumn::kGameRatingColumn:
return QVariant(); // TODO
case GameColumn::kGameRegionColumn:
if (role == Qt::DisplayRole) {
auto region = RegionStringMap.find(entry.regions());
if (region != RegionStringMap.end()) {
return region->second;
}
}
break;
case GameColumn::kCompatabilityColumn:
return QVariant(); // TODO
case GameColumn::kPlayerCountColumn:
if (role == Qt::DisplayRole) {
return QString::number(entry.player_count());
}
break;
}
return QVariant();
}
QVariant XGameLibraryModel::headerData(int section, Qt::Orientation orientation,
int role) const {
if (orientation == Qt::Vertical || role != Qt::DisplayRole) {
return QVariant();
}
GameColumn column = (GameColumn)section;
switch (column) {
case GameColumn::kIconColumn:
return tr("");
case GameColumn::kTitleColumn:
return tr("Title");
case GameColumn::kTitleIdColumn:
return tr("Title ID");
case GameColumn::kMediaIdColumn:
return tr("Media ID");
case GameColumn::kPathColumn:
return tr("Path");
case GameColumn::kVersionColumn:
return tr("Version");
case GameColumn::kGenreColumn:
return tr("Genre");
case GameColumn::kReleaseDateColumn:
return tr("Release Date");
case GameColumn::kBuildDateColumn:
return tr("Build Date");
case GameColumn::kLastPlayedColumn:
return tr("Last Played");
case GameColumn::kTimePlayedColumn:
return tr("Time Played");
case GameColumn::kAchievementsUnlockedColumn:
return tr("Achievements");
case GameColumn::kGamerscoreUnlockedColumn:
return tr("Gamerscore");
case GameColumn::kGameRatingColumn:
return tr("Rating");
case GameColumn::kGameRegionColumn:
return tr("Region");
case GameColumn::kCompatabilityColumn:
return tr("Compatibility");
case GameColumn::kPlayerCountColumn:
return tr("# Players");
default:
return QVariant(); // Should not be seeing this
}
}
int XGameLibraryModel::rowCount(const QModelIndex& parent) const {
if (parent.isValid()) { // TODO
return 0;
}
return (int)library_->size();
}
int XGameLibraryModel::columnCount(const QModelIndex& parent) const {
return (int)GameColumn::kColumnCount;
}
void XGameLibraryModel::refresh() {
beginResetModel();
endResetModel();
}
} // namespace qt
} // namespace ui
} // namespace xe

View File

@ -0,0 +1,72 @@
#ifndef XENIA_UI_QT_GAME_LIBRARY_MODEL_H_
#define XENIA_UI_QT_GAME_LIBRARY_MODEL_H_
#include "xenia/app/library/game_library.h"
#include <QAbstractTableModel>
#include <memory>
namespace xe {
namespace ui {
namespace qt {
using app::XGameEntry;
using app::XGameLibrary;
enum class GameColumn {
kIconColumn,
kTitleColumn,
kTitleIdColumn,
kMediaIdColumn,
kPathColumn,
kVersionColumn,
kGenreColumn,
kReleaseDateColumn,
kBuildDateColumn,
kLastPlayedColumn,
kTimePlayedColumn,
kAchievementsUnlockedColumn,
kGamerscoreUnlockedColumn,
kGameRatingColumn,
kGameRegionColumn,
kCompatabilityColumn,
kPlayerCountColumn,
kColumnCount, // For column counting, unused, keep as last entry
};
static std::map<app::XGameRegions, QString> RegionStringMap{
{XEX_REGION_ALL, "Region Free"},
{XEX_REGION_NTSCJ, "NTSC-J"},
{XEX_REGION_NTSCJ_CHINA, "NTSC-J (China)"},
{XEX_REGION_NTSCJ_JAPAN, "JTSC-J (Japan)"},
{XEX_REGION_NTSCU, "NTSC-U"},
{XEX_REGION_OTHER, "Other"},
{XEX_REGION_PAL, "PAL"},
{XEX_REGION_PAL_AU_NZ, "PAL (AU/NZ)"},
};
class XGameLibraryModel final : public QAbstractTableModel {
Q_OBJECT
public:
explicit XGameLibraryModel(QObject* parent = nullptr);
// QAbstractTableModel Implementation
QVariant data(const QModelIndex& index,
int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex& parent) const override;
int columnCount(const QModelIndex& parent) const override;
void refresh();
private:
XGameLibrary* library_;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

View File

@ -0,0 +1,26 @@
project_root = "../../../.."
include(project_root.."/tools/build")
local qt = premake.extensions.qt
group("src")
project("xenia-ui-qt")
uuid("3AB69653-3ACB-4C0B-976C-3B7F044E3E3A")
kind("StaticLib")
language("C++")
-- Setup Qt libraries
qt.enable()
qtmodules{"core", "gui", "widgets"}
qtprefix "Qt5"
configuration {"Debug"}
qtsuffix "d"
configuration {}
links({
"xenia-base",
"xenia-core",
})
defines({
})
recursive_platform_files()

View File

@ -0,0 +1,600 @@
#include "xenia/ui/qt/tabs/debug_tab.h"
#include <QButtonGroup>
#include <QGraphicsEffect>
#include <QHBoxLayout>
#include <QMenu>
#include "xenia/ui/qt/widgets/checkbox.h"
#include "xenia/ui/qt/widgets/combobox.h"
#include "xenia/ui/qt/widgets/dropdown_button.h"
#include "xenia/ui/qt/widgets/groupbox.h"
#include "xenia/ui/qt/widgets/line_edit.h"
#include "xenia/ui/qt/widgets/push_button.h"
#include "xenia/ui/qt/widgets/radio_button.h"
#include "xenia/ui/qt/widgets/scroll_area.h"
#include "xenia/ui/qt/widgets/separator.h"
#include "xenia/ui/qt/widgets/slider.h"
#include "xenia/ui/qt/widgets/tab_selector.h"
#include "xenia/ui/qt/widgets/text_edit.h"
namespace xe {
namespace ui {
namespace qt {
// sidebar_->addAction(0xE90F, "Components");
// sidebar_->addAction(0xE700, "Navigation");
// sidebar_->addAction(0xE790, "Theme");
// sidebar_->addAction(0xE8F1, "Library");
DebugTab::DebugTab() : XTab("Debug", "DebugTab") {
sidebar_items_ =
QList<SidebarItem>{{0xE90F, "Components", CreateComponentsTab()},
{0xE700, "Navigation", CreateNavigationTab()},
{0xE790, "Theme", CreateThemeTab()},
{0xE8F1, "Library", CreateLibraryTab()}};
Build();
}
void DebugTab::Build() {
layout_ = new QHBoxLayout();
layout_->setContentsMargins(0, 0, 0, 0);
layout_->setSpacing(0);
setLayout(layout_);
BuildSidebar();
content_widget_ = new QStackedWidget();
for (const SidebarItem& item : sidebar_items_) {
content_widget_->addWidget(item.widget);
}
layout_->addWidget(content_widget_);
}
void DebugTab::BuildSidebar() {
sidebar_container_ = new QWidget(this);
sidebar_container_->setObjectName("sidebarContainer");
QVBoxLayout* sidebar_layout = new QVBoxLayout;
sidebar_layout->setMargin(0);
sidebar_layout->setSpacing(0);
sidebar_container_->setLayout(sidebar_layout);
// Add drop shadow to sidebar widget
QGraphicsDropShadowEffect* effect = new QGraphicsDropShadowEffect;
effect->setBlurRadius(16);
effect->setXOffset(4);
effect->setYOffset(0);
effect->setColor(QColor(0, 0, 0, 64));
sidebar_container_->setGraphicsEffect(effect);
// Create sidebar
sidebar_ = new XSideBar;
sidebar_->setOrientation(Qt::Vertical);
sidebar_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
sidebar_->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
// Create sidebar title
QWidget* sidebar_title = new QWidget;
sidebar_title->setObjectName("sidebarTitle");
QVBoxLayout* title_layout = new QVBoxLayout;
title_layout->setMargin(0);
title_layout->setContentsMargins(0, 40, 0, 0);
title_layout->setSpacing(0);
sidebar_title->setLayout(title_layout);
// Title labels
QLabel* xenia_title = new QLabel("Debug");
xenia_title->setObjectName("sidebarTitleLabel");
xenia_title->setAlignment(Qt::AlignCenter);
title_layout->addWidget(xenia_title, 0, Qt::AlignCenter);
// Title separator
auto separator = new XSeparator;
title_layout->addSpacing(32);
title_layout->addWidget(separator, 0, Qt::AlignCenter);
// Setup Sidebar toolbar
sidebar_->addWidget(sidebar_title);
sidebar_->addSpacing(20);
QButtonGroup* bg = new QButtonGroup();
// loop over sidebar button items and connect them to slots
int counter = 0;
for (auto it = sidebar_items_.begin(); it != sidebar_items_.end();
++it, ++counter) {
SidebarItem& item = *it;
auto btn = sidebar_->addAction(item.glyph, item.name);
btn->setCheckable(true);
bg->addButton(btn);
// set the first item to checked
if (counter == 0) {
btn->setChecked(true);
}
// link up the clicked signal
connect(btn, &XSideBarButton::clicked,
[&]() { content_widget_->setCurrentWidget(item.widget); });
}
sidebar_layout->addWidget(sidebar_, 0, Qt::AlignHCenter | Qt::AlignTop);
sidebar_layout->addStretch(1);
// Add sidebar to tab widget
layout_->addWidget(sidebar_container_, 0, Qt::AlignLeft);
}
QWidget* DebugTab::CreateComponentsTab() {
QWidget* w = new QWidget();
w->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::MinimumExpanding);
XScrollArea* scroll_area = new XScrollArea(this);
scroll_area->setWidget(w);
QVBoxLayout* layout = new QVBoxLayout();
w->setLayout(layout);
layout->setSpacing(16);
layout->setContentsMargins(0, 16, 0, 0);
layout->addWidget(CreateButtonGroup());
layout->addWidget(CreateSliderGroup());
layout->addWidget(CreateCheckboxGroup());
layout->addWidget(CreateRadioButtonGroup());
layout->addWidget(CreateInputGroup());
layout->addStretch();
return scroll_area;
}
QWidget* DebugTab::CreateNavigationTab() {
QWidget* w = new QWidget();
QVBoxLayout* layout = new QVBoxLayout(w);
layout->setSpacing(0);
layout->setContentsMargins(64, 64, 64, 64);
QWidget* container = new QWidget();
container->setObjectName("navigationContainer");
container->setFixedSize(640, 480);
QVBoxLayout* container_layout = new QVBoxLayout(container);
container_layout->setSpacing(4);
container_layout->setContentsMargins(0, 0, 0, 0);
container->setLayout(container_layout);
XTab *tab1 = new XTab("Tab1"), *tab2 = new XTab("Tab2"),
*tab3 = new XTab("Tab3");
QVBoxLayout *tab1_layout = new QVBoxLayout(tab1),
*tab2_layout = new QVBoxLayout(tab2),
*tab3_layout = new QVBoxLayout(tab3);
std::vector<XTab*> tabs{tab1, tab2, tab3};
XTabSelector* tab_selector = new XTabSelector(tabs);
container_layout->addWidget(tab_selector, 0, Qt::AlignCenter);
QStackedLayout* content_layout = new QStackedLayout();
content_layout->setSpacing(0);
content_layout->setContentsMargins(0, 0, 0, 0);
container_layout->addLayout(content_layout);
tab1_layout->addWidget(CreateTab1Widget(tab_selector, content_layout));
QWidget* test2 = new QWidget();
test2->setStyleSheet("background:blue");
tab2_layout->addWidget(test2);
QWidget* test3 = new QWidget();
test3->setStyleSheet("background:green");
tab3_layout->addWidget(test3);
for (XTab* tab : tabs) {
QVBoxLayout* tab_layout = qobject_cast<QVBoxLayout*>(tab->layout());
tab_layout->setContentsMargins(0, 0, 0, 0);
tab_layout->setSpacing(0);
content_layout->addWidget(tab);
}
content_layout->setCurrentWidget(tab1);
connect(tab_selector, &XTabSelector::TabChanged, [content_layout](XTab* tab) {
content_layout->setCurrentWidget(tab);
});
layout->addWidget(container, 0, Qt::AlignHCenter | Qt::AlignTop);
return w;
}
QWidget* DebugTab::CreateThemeTab() {
QWidget* w = new QWidget();
w->setStyleSheet("background: #505050");
QVBoxLayout* layout = new QVBoxLayout(w);
layout->setContentsMargins(64, 64, 64, 64);
ThemeManager& theme_manager = ThemeManager::Instance();
Theme& theme = theme_manager.current_theme();
QLabel* label = new QLabel(
QStringLiteral("Current Theme: %1\n Description: %2\n Path: %3")
.arg(theme.config().name(), theme.config().description(),
theme.directory()));
layout->addWidget(label);
return w;
}
QWidget* DebugTab::CreateLibraryTab() {
QWidget* w = new QWidget();
w->setStyleSheet("background: yellow;");
return w;
}
QWidget* DebugTab::CreateButtonGroup() {
QWidget* group = new QWidget();
QVBoxLayout* group_layout = new QVBoxLayout();
group_layout->setContentsMargins(32, 0, 32, 0);
group_layout->setSpacing(16);
group->setLayout(group_layout);
XGroupBox* groupbox = new XGroupBox("Buttons");
QVBoxLayout* groupbox_layout = new QVBoxLayout();
groupbox_layout->setContentsMargins(16, 16, 16, 16);
groupbox->setLayout(groupbox_layout);
group_layout->addWidget(groupbox);
QLabel* pushbtn_label = new QLabel("Push Buttons");
groupbox_layout->addWidget(pushbtn_label);
QHBoxLayout* pushbtn_layout = new QHBoxLayout();
pushbtn_layout->setSpacing(32);
pushbtn_layout->setContentsMargins(0, 0, 0, 0);
QSize btn_size(120, 40);
XPushButton* pushbtn1 = new XPushButton("Push Button");
pushbtn1->setMinimumSize(btn_size);
XPushButton* pushbtn2 = new XPushButton("Disabled");
pushbtn2->setDisabled(true);
pushbtn2->setMinimumSize(btn_size);
pushbtn_layout->addWidget(pushbtn1);
pushbtn_layout->addWidget(pushbtn2);
pushbtn_layout->addStretch();
groupbox_layout->addLayout(pushbtn_layout);
QLabel* dropdown_btn_label = new QLabel("Dropdown Buttons");
groupbox_layout->addWidget(dropdown_btn_label);
QHBoxLayout* dropdown_btn_layout = new QHBoxLayout();
dropdown_btn_layout->setSpacing(32);
dropdown_btn_layout->setContentsMargins(0, 0, 0, 0);
QMenu* menu = new QMenu(this);
menu->addAction("Test");
menu->addSeparator();
menu->addAction("Close");
XDropdownButton* dropdown_btn = new XDropdownButton("Dropdown Button", menu);
dropdown_btn->setMinimumSize(btn_size);
XDropdownButton* dropdown_btn_disabled = new XDropdownButton("Disabled");
dropdown_btn_disabled->setDisabled(true);
dropdown_btn_disabled->setMinimumSize(btn_size);
dropdown_btn_layout->addWidget(dropdown_btn);
dropdown_btn_layout->addWidget(dropdown_btn_disabled);
dropdown_btn_layout->addStretch();
groupbox_layout->addLayout(dropdown_btn_layout);
QLabel* combobox_label = new QLabel("Combo Boxes");
groupbox_layout->addWidget(combobox_label);
QHBoxLayout* combobox_layout = new QHBoxLayout();
combobox_layout->setSpacing(32);
combobox_layout->setContentsMargins(0, 0, 0, 0);
QSize combobox_size = QSize(120, 32);
XComboBox* combobox1 = new XComboBox();
combobox1->setMinimumSize(combobox_size);
combobox1->addItem("Simple String 1");
combobox1->addItem("Simple String 2");
combobox_layout->addWidget(combobox1);
XComboBox* combobox2 = new XComboBox();
combobox2->setMinimumSize(combobox_size);
QAction* action1 = new QAction("Action 1");
QAction* action2 = new QAction("Action 2");
combobox2->addItem(action1->text(), QVariant::fromValue(action1));
combobox2->addItem(action2->text(), QVariant::fromValue(action2));
combobox2->addAction(action2);
combobox_layout->addWidget(combobox2);
combobox_layout->addStretch();
groupbox_layout->addLayout(combobox_layout);
return group;
}
QWidget* DebugTab::CreateSliderGroup() {
QWidget* group = new QWidget();
QVBoxLayout* group_layout = new QVBoxLayout();
group_layout->setContentsMargins(32, 0, 32, 0);
group_layout->setSpacing(16);
group->setLayout(group_layout);
XGroupBox* groupbox = new XGroupBox("Sliders");
QHBoxLayout* groupbox_layout = new QHBoxLayout();
groupbox_layout->setContentsMargins(16, 16, 16, 16);
groupbox->setLayout(groupbox_layout);
group_layout->addWidget(groupbox);
// horizontal slider
XSlider* horizontal_slider = new XSlider();
horizontal_slider->setFixedWidth(120);
QLabel* horizontal_label = new QLabel();
horizontal_label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Ignored);
connect(horizontal_slider, &XSlider::valueChanged, [=](int value) {
QString text;
horizontal_label->setText(text.asprintf("Value: %02d", value));
});
horizontal_slider->valueChanged(0);
groupbox_layout->addWidget(horizontal_slider);
groupbox_layout->addWidget(horizontal_label);
groupbox_layout->addSpacing(16);
// vertical slider
XSlider* vertical_slider = new XSlider(Qt::Vertical);
vertical_slider->setFixedSize(20, 60);
// vertical slider causes issues in a vertical orientation right now
// TODO: fix this. for now just ignore its vertical size
vertical_slider->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Ignored);
QLabel* vertical_label = new QLabel();
connect(vertical_slider, &XSlider::valueChanged, [=](int value) {
QString text;
vertical_label->setText(text.asprintf("Value: %02d", value));
});
vertical_slider->valueChanged(0);
groupbox_layout->addWidget(vertical_slider);
groupbox_layout->addWidget(vertical_label);
groupbox_layout->addStretch();
return group;
}
QWidget* DebugTab::CreateCheckboxGroup() {
QWidget* group = new QWidget();
QVBoxLayout* group_layout = new QVBoxLayout();
group_layout->setContentsMargins(32, 0, 32, 0);
group_layout->setSpacing(16);
group->setLayout(group_layout);
XGroupBox* groupbox = new XGroupBox("Checkboxes");
QVBoxLayout* groupbox_layout = new QVBoxLayout();
groupbox_layout->setContentsMargins(16, 16, 16, 16);
groupbox->setLayout(groupbox_layout);
group_layout->addWidget(groupbox);
QHBoxLayout* layer_1_layout = new QHBoxLayout();
layer_1_layout->setContentsMargins(0, 0, 0, 0);
layer_1_layout->setSpacing(20);
QHBoxLayout* layer_2_layout = new QHBoxLayout();
layer_2_layout->setContentsMargins(0, 0, 0, 0);
layer_2_layout->setSpacing(20);
groupbox_layout->addLayout(layer_1_layout);
groupbox_layout->addLayout(layer_2_layout);
group_layout->addLayout(groupbox_layout);
XCheckBox* checkbox1 = new XCheckBox();
checkbox1->setText("Test Checkbox");
XCheckBox* checkbox2 = new XCheckBox();
checkbox2->set_custom(true);
checkbox2->set_checked_color(QColor(255, 150, 100));
checkbox2->setText("Alternate Color");
layer_1_layout->addWidget(checkbox1);
layer_1_layout->addWidget(checkbox2);
layer_1_layout->addStretch();
XCheckBox* checkbox3 = new XCheckBox();
checkbox3->setText("Checkbox with really long text to test truncation");
layer_2_layout->addWidget(checkbox3);
return group;
}
QWidget* DebugTab::CreateRadioButtonGroup() {
QWidget* group = new QWidget();
QVBoxLayout* group_layout = new QVBoxLayout();
group_layout->setContentsMargins(32, 0, 32, 0);
group_layout->setSpacing(0);
group->setLayout(group_layout);
XGroupBox* groupbox = new XGroupBox("Radio Buttons");
QVBoxLayout* groupbox_layout = new QVBoxLayout();
groupbox_layout->setContentsMargins(16, 16, 16, 16);
groupbox->setLayout(groupbox_layout);
group_layout->addWidget(groupbox);
QHBoxLayout* layer_1_layout = new QHBoxLayout();
layer_1_layout->setContentsMargins(0, 0, 0, 0);
layer_1_layout->setSpacing(20);
QHBoxLayout* layer_2_layout = new QHBoxLayout();
layer_2_layout->setContentsMargins(0, 0, 0, 0);
layer_2_layout->setSpacing(20);
groupbox_layout->addLayout(layer_1_layout);
groupbox_layout->addLayout(layer_2_layout);
group_layout->addLayout(groupbox_layout);
XRadioButton* radio1 = new XRadioButton();
radio1->setText("Test Radio Button 1");
XRadioButton* radio2 = new XRadioButton();
radio2->setText("Test Radio Button 2");
layer_1_layout->addWidget(radio1);
layer_1_layout->addWidget(radio2);
layer_1_layout->addStretch();
XRadioButton* radio3 = new XRadioButton();
radio3->setText("Radio Button with really long text to test truncation");
radio3->set_custom(true);
radio3->set_checked_color(QColor(255, 150, 100));
XRadioButton* radio4 = new XRadioButton();
radio4->setText("Error");
radio4->set_custom(true);
radio4->set_checked_color(QColor(255, 0, 0));
layer_2_layout->addWidget(radio3);
layer_2_layout->addWidget(radio4);
layer_2_layout->addStretch();
// add radio buttons to their respective groups
QButtonGroup* bg1 = new QButtonGroup();
QButtonGroup* bg2 = new QButtonGroup();
bg1->addButton(radio1);
bg1->addButton(radio2);
bg2->addButton(radio3);
bg2->addButton(radio4);
return group;
}
QWidget* DebugTab::CreateInputGroup() {
QWidget* group = new QWidget();
QVBoxLayout* group_layout = new QVBoxLayout();
group_layout->setContentsMargins(32, 0, 32, 0);
group_layout->setSpacing(0);
group->setLayout(group_layout);
XGroupBox* groupbox = new XGroupBox("Input Boxes");
QVBoxLayout* groupbox_layout = new QVBoxLayout();
groupbox_layout->setContentsMargins(16, 16, 16, 16);
groupbox->setLayout(groupbox_layout);
QLabel* lineedit_label = new QLabel("Line Edit");
groupbox_layout->addWidget(lineedit_label);
QHBoxLayout* line_layout = new QHBoxLayout();
XLineEdit* line_edit = new XLineEdit("Text...");
XLineEdit* disabled_line = new XLineEdit("Disabled");
disabled_line->setDisabled(true);
line_layout->addWidget(line_edit);
line_layout->addWidget(disabled_line);
groupbox_layout->addLayout(line_layout);
QLabel* textedit_label = new QLabel("Text Edit");
groupbox_layout->addWidget(textedit_label);
QHBoxLayout* text_layout = new QHBoxLayout();
XTextEdit* text_edit = new XTextEdit("Text...");
text_edit->setMaximumHeight(80);
XTextEdit* disabled_edit = new XTextEdit("Disabled");
disabled_edit->setDisabled(true);
disabled_edit->setMaximumHeight(80);
text_layout->addWidget(text_edit);
text_layout->addWidget(disabled_edit);
groupbox_layout->addLayout(text_layout);
group_layout->addWidget(groupbox);
return group;
}
QWidget* DebugTab::CreateTab1Widget(XTabSelector* tab_selector,
QStackedLayout* tab_stack_layout) {
QWidget* widget = new QWidget();
QVBoxLayout* layout = new QVBoxLayout(widget);
layout->setSpacing(0);
layout->setContentsMargins(16, 16, 16, 16);
XPushButton* changeTabButton = new XPushButton("Go to Tab 2");
changeTabButton->setMinimumSize(100, 24);
connect(changeTabButton, &QPushButton::clicked, [=]() {
tab_selector->SetTabIndex(1);
tab_stack_layout->setCurrentIndex(1);
});
layout->addWidget(changeTabButton, 0, Qt::AlignCenter);
layout->addStretch();
return widget;
}
QWidget* DebugTab::CreateTab2Widget(XTabSelector* tab_selector,
QStackedLayout* tab_stack_layout) {
return nullptr;
}
QWidget* DebugTab::CreateTab3Widget(XTabSelector* tab_selector,
QStackedLayout* tab_stack_layout) {
return nullptr;
}
} // namespace qt
} // namespace ui
} // namespace xe

View File

@ -0,0 +1,65 @@
#ifndef XENIA_UI_QT_TABS_DEBUG_H_
#define XENIA_UI_QT_TABS_DEBUG_H_
#include <QHBoxLayout>
#include <QStackedLayout>
#include <QStackedWidget>
#include "xenia/ui/qt/widgets/sidebar.h"
#include "xenia/ui/qt/widgets/tab.h"
namespace xe {
namespace ui {
namespace qt {
class XTabSelector;
// TODO: should this be in its own file for reusability?
// Represents a sidebar item and a widget that is shown when the item is clicked
struct SidebarItem {
QChar glyph;
const char* name;
QWidget* widget;
};
class DebugTab : public XTab {
Q_OBJECT
public:
explicit DebugTab();
private:
void Build();
void BuildSidebar();
QWidget* CreateComponentsTab();
QWidget* CreateNavigationTab();
QWidget* CreateThemeTab();
QWidget* CreateLibraryTab();
// create widgets for "components" tab
QWidget* CreateButtonGroup();
QWidget* CreateSliderGroup();
QWidget* CreateCheckboxGroup();
QWidget* CreateRadioButtonGroup();
QWidget* CreateInputGroup();
// create widgets for "navigation" tab
QWidget* CreateTab1Widget(XTabSelector* tab_selector,
QStackedLayout* tab_stack_layout);
QWidget* CreateTab2Widget(XTabSelector* tab_selector,
QStackedLayout* tab_stack_layout);
QWidget* CreateTab3Widget(XTabSelector* tab_selector,
QStackedLayout* tab_stack_layout);
QHBoxLayout* layout_ = nullptr;
QWidget* sidebar_container_ = nullptr;
XSideBar* sidebar_ = nullptr;
QStackedWidget* content_widget_ = nullptr;
QList<SidebarItem> sidebar_items_;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

View File

@ -0,0 +1,255 @@
#include "xenia/ui/qt/tabs/home_tab.h"
#include <QFIleDialog>
#include <QGraphicsEffect>
#include <QProgressBar>
#include "xenia/app/emulator_window.h"
#include "xenia/base/logging.h"
#include "xenia/ui/qt/actions/action.h"
#include "xenia/ui/qt/main_window.h"
#include "xenia/ui/qt/widgets/separator.h"
#include "xenia/ui/qt/widgets/slider.h"
namespace xe {
namespace ui {
namespace qt {
HomeTab::HomeTab() : XTab("Home", "HomeTab") { Build(); }
void HomeTab::Build() {
layout_ = new QHBoxLayout();
layout_->setMargin(0);
layout_->setSpacing(0);
setLayout(layout_);
BuildSidebar();
BuildRecentView();
}
void HomeTab::BuildSidebar() {
// sidebar container widget
sidebar_ = new QWidget(this);
sidebar_->setObjectName("sidebarContainer");
QVBoxLayout* sidebar_layout = new QVBoxLayout;
sidebar_layout->setMargin(0);
sidebar_layout->setSpacing(0);
sidebar_->setLayout(sidebar_layout);
// Add drop shadow to sidebar widget
QGraphicsDropShadowEffect* effect = new QGraphicsDropShadowEffect;
effect->setBlurRadius(16);
effect->setXOffset(4);
effect->setYOffset(0);
effect->setColor(QColor(0, 0, 0, 64));
sidebar_->setGraphicsEffect(effect);
// Create sidebar
sidebar_toolbar_ = new XSideBar;
sidebar_toolbar_->setOrientation(Qt::Vertical);
sidebar_toolbar_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
sidebar_toolbar_->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
// Create sidebar title
QWidget* sidebar_title = new QWidget;
sidebar_title->setObjectName("sidebarTitle");
QVBoxLayout* title_layout = new QVBoxLayout;
title_layout->setMargin(0);
title_layout->setContentsMargins(0, 40, 0, 0);
title_layout->setSpacing(0);
sidebar_title->setLayout(title_layout);
// Title labels
QLabel* xenia_title = new QLabel("Xenia");
xenia_title->setObjectName("sidebarTitleLabel");
QLabel* xenia_subtitle = new QLabel("Xbox 360 Emulator");
xenia_subtitle->setObjectName("sidebarSubtitleLabel");
title_layout->addWidget(xenia_title, 0, Qt::AlignHCenter | Qt::AlignBottom);
title_layout->addWidget(xenia_subtitle, 0, Qt::AlignHCenter | Qt::AlignTop);
// Title separator
auto separator = new XSeparator;
title_layout->addWidget(separator, 0, Qt::AlignHCenter);
// Setup Sidebar toolbar
sidebar_toolbar_->addWidget(sidebar_title);
sidebar_toolbar_->addSpacing(20);
auto open_file_btn = sidebar_toolbar_->addAction(0xE838, "Open File");
connect(open_file_btn, &XSideBarButton::clicked, this,
&HomeTab::OpenFileTriggered);
auto import_folder_btn = sidebar_toolbar_->addAction(0xE8F4, "Import Folder");
connect(import_folder_btn, &XSideBarButton::clicked, this,
&HomeTab::ImportFolderTriggered);
sidebar_toolbar_->addSeparator();
sidebar_layout->addWidget(sidebar_toolbar_, 0,
Qt::AlignHCenter | Qt::AlignTop);
sidebar_layout->addStretch(1);
// Add sidebar to tab widget
layout_->addWidget(sidebar_, 0, Qt::AlignLeft);
}
void HomeTab::BuildRecentView() {
// Create container widget
QWidget* recent_container = new QWidget(this);
QVBoxLayout* recent_layout = new QVBoxLayout(this);
recent_layout->setContentsMargins(0, 0, 0, 0);
recent_layout->setSpacing(0);
recent_container->setLayout(recent_layout);
// Setup toolbar
auto toolbar = recent_toolbar_;
toolbar = new XToolBar(this);
QLabel* title = new QLabel("Recent Games");
title->setObjectName("recentGames");
toolbar->addWidget(title);
toolbar->addSeparator();
// TODO: handle button clicks
auto play_btn = toolbar->addAction(new XAction(QChar(0xEDB5), "Play"));
connect(play_btn, &XToolBarItem::clicked, [this]() { PlayTriggered(); });
toolbar->addAction(new XAction(QChar(0xEBE8), "Debug"));
toolbar->addAction(new XAction(QChar(0xE946), "Info"));
toolbar->addSeparator();
toolbar->addAction(new XAction(QChar(0xE8FD), "List"));
toolbar->addAction(new XAction(QChar(0xF0E2), "Grid"));
// TODO: hide slider unless "Grid" mode is selected
auto* slider = new XSlider(Qt::Horizontal, this);
slider->setRange(48, 96);
slider->setFixedWidth(100);
toolbar->addWidget(slider);
recent_layout->addWidget(toolbar);
// Create recent games list view
// TODO: this should only be shown when "List" mode selected in toolbar
// and should also only load games from a "recent" cache
list_view_ = new XGameListView(this);
recent_layout->addWidget(list_view_);
layout_->addWidget(recent_container);
// Lower the widget to prevent overlap with sidebar's shadow
recent_container->lower();
}
void HomeTab::PlayTriggered() {
// Get path from table and launch game in new EmulatorWindow
// This is purely a proof of concept
auto index = list_view_->selectionModel();
if (index->hasSelection()) {
QModelIndexList path_row =
index->selectedRows(static_cast<int>(GameColumn::kPathColumn));
const QModelIndex& path_index = path_row.at(0);
QString path = path_index.data().toString();
/*wchar_t* title_w = new wchar_t[title.length() + 1];
title.toWCharArray(title_w);
title_w[title.length()] = '\0';*/
auto win = qobject_cast<QtWindow*>(window());
app::EmulatorWindow* wnd = new app::EmulatorWindow(win->loop(), "");
/*wnd->resize(1280, 720);
wnd->show();*/
win->setCentralWidget(wnd);
wnd->Launch(path.toStdString());
}
}
void HomeTab::OpenFileTriggered() {
QString file_name = QFileDialog::getOpenFileName(
this, "Open Game", "",
tr("Xbox 360 Executable (*.xex);;Disc Image (*.iso);;All Files (*)"));
if (!file_name.isEmpty()) {
// this manual conversion seems to be required as Qt's std::(w)string impl
// and the one i've been linking to seem incompatible
wchar_t* path_w = new wchar_t[file_name.length() + 1];
file_name.toWCharArray(path_w);
path_w[file_name.length()] = '\0';
XGameLibrary* lib = XGameLibrary::Instance();
lib->ScanPath(path_w);
list_view_->RefreshGameList();
}
}
void HomeTab::ImportFolderTriggered() {
QString path = QFileDialog::getExistingDirectory(this, "Open Folder", "");
if (!path.isEmpty()) {
// this manual conversion seems to be required as Qt's std::(w)string impl
// and the one i've been linking to seem incompatible
wchar_t* path_w = new wchar_t[path.length() + 1];
path.toWCharArray(path_w);
path_w[path.length()] = '\0';
QWidget* progress_widget = new QWidget();
QHBoxLayout* layout = new QHBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(16);
progress_widget->setLayout(layout);
QLabel* label = new QLabel("Scanning directories...");
label->setStyleSheet("color: #c7c7c7");
layout->addWidget(label);
QProgressBar* bar = new QProgressBar();
bar->setFixedSize(120, 16);
bar->setRange(0, 100);
bar->setValue(0);
bar->setTextVisible(false);
layout->addWidget(bar);
layout->addStretch();
MainWindow* window = qobject_cast<MainWindow*>(this->window());
window->AddStatusBarWidget(progress_widget);
XGameLibrary* lib = XGameLibrary::Instance();
lib->ScanPathAsync(path_w, [=](double progress, const XGameEntry& entry) {
// update progress bar on main UI thread
QMetaObject::invokeMethod(
bar,
[=]() {
bar->setValue(progress);
if (progress == 100.0) {
window->RemoveStatusBarWidget(progress_widget);
}
},
Qt::QueuedConnection);
// Just a PoC. In future change to refresh list
// when all games added.
list_view_->RefreshGameList();
});
}
}
} // namespace qt
} // namespace ui
} // namespace xe

View File

@ -0,0 +1,41 @@
#ifndef XENIA_UI_QT_TABS_HOME_H_
#define XENIA_UI_QT_TABS_HOME_H_
#include <QToolBar>
#include "xenia/ui/qt/widgets/game_listview.h"
#include "xenia/ui/qt/widgets/sidebar.h"
#include "xenia/ui/qt/widgets/sidebar_button.h"
#include "xenia/ui/qt/widgets/tab.h"
#include "xenia/ui/qt/widgets/toolbar.h"
namespace xe {
namespace ui {
namespace qt {
class HomeTab : public XTab {
Q_OBJECT
public:
explicit HomeTab();
public slots:
void PlayTriggered();
void OpenFileTriggered();
void ImportFolderTriggered();
private:
void Build();
void BuildSidebar();
void BuildRecentView();
QHBoxLayout* layout_ = nullptr;
QWidget* sidebar_ = nullptr;
XSideBar* sidebar_toolbar_ = nullptr;
XToolBar* recent_toolbar_ = nullptr;
XGameListView* list_view_ = nullptr;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif XENIA_UI_QT_TABS_HOME_H_

View File

@ -0,0 +1,58 @@
#include "xenia/ui/qt/tabs/library_tab.h"
#include "xenia/ui/qt/actions/add_game.h"
#include "xenia/ui/qt/widgets/slider.h"
namespace xe {
namespace ui {
namespace qt {
LibraryTab::LibraryTab() : XTab("Library") {
Build();
return;
}
void LibraryTab::Build() {
layout_ = new QVBoxLayout();
layout_->setMargin(0);
layout_->setSpacing(0);
setLayout(layout_);
BuildToolBar();
BuildListView();
connect(slider_, SIGNAL(valueChanged(int)), list_view_,
SLOT(setRowSize(int)));
}
void LibraryTab::BuildToolBar() {
toolbar_ = new XToolBar(this);
toolbar_->setFixedHeight(46);
layout_->addWidget(toolbar_);
toolbar_->addAction(new XAddGameAction());
toolbar_->addAction(new XAction(QChar(0xF12B), "Add Folder"));
toolbar_->addSeparator();
toolbar_->addAction(new XAction(QChar(0xEDB5), "Play"));
toolbar_->addAction(new XAction(QChar(0xEBE8), "Debug"));
toolbar_->addAction(new XAction(QChar(0xE946), "Info"));
toolbar_->addSeparator();
toolbar_->addAction(new XAction(QChar(0xE8FD), "List"));
toolbar_->addAction(new XAction(QChar(0xF0E2), "Grid"));
slider_ = new XSlider(Qt::Horizontal, this);
slider_->setRange(48,96);
slider_->setFixedWidth(100);
toolbar_->addWidget(slider_);
}
void LibraryTab::BuildListView() {
list_view_ = new XGameListView(this);
layout_->addWidget(list_view_);
}
} // namespace qt
} // namespace ui
} // namespace xe

View File

@ -0,0 +1,32 @@
#ifndef XENIA_UI_QT_TABS_LIBRARY_H_
#define XENIA_UI_QT_TABS_LIBRARY_H_
#include "xenia/ui/qt/widgets/game_listview.h"
#include "xenia/ui/qt/widgets/tab.h"
#include "xenia/ui/qt/widgets/toolbar.h"
namespace xe {
namespace ui {
namespace qt {
class LibraryTab : public XTab {
Q_OBJECT
public:
explicit LibraryTab();
private:
void Build();
void BuildToolBar();
void BuildListView();
QVBoxLayout* layout_ = nullptr;
XToolBar* toolbar_ = nullptr;
XGameListView* list_view_ = nullptr;
QSlider* slider_ = nullptr;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

View File

@ -0,0 +1,16 @@
#include "advanced_pane.h"
namespace xe {
namespace ui {
namespace qt {
void AdvancedPane::Build() {
QWidget* widget = new QWidget();
widget->setStyleSheet("background: green");
set_widget(widget);
}
} // namespace qt
} // namespace ui
} // namespace xe

View File

@ -0,0 +1,22 @@
#ifndef XENIA_UI_QT_ADVANCED_PANE_H_
#define XENIA_UI_QT_ADVANCED_PANE_H_
#include "settings_pane.h"
namespace xe {
namespace ui {
namespace qt {
class AdvancedPane : public SettingsPane {
Q_OBJECT
public:
explicit AdvancedPane() : SettingsPane(0xE7BA, "Advanced") {}
void Build() override;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

View File

@ -0,0 +1,16 @@
#include "controls_pane.h"
namespace xe {
namespace ui {
namespace qt {
void ControlsPane::Build() {
QWidget* widget = new QWidget();
widget->setStyleSheet("background: yellow");
set_widget(widget);
}
} // namespace qt
} // namespace ui
} // namespace xe

View File

@ -0,0 +1,22 @@
#ifndef XENIA_UI_QT_CONTROLS_PANE_H_
#define XENIA_UI_QT_CONTROLS_PANE_H_
#include "settings_pane.h"
namespace xe {
namespace ui {
namespace qt {
class ControlsPane : public SettingsPane {
Q_OBJECT
public:
explicit ControlsPane() : SettingsPane(0xE7FC, "Controls") {}
void Build() override;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

View File

@ -0,0 +1,16 @@
#include "cpu_pane.h"
namespace xe {
namespace ui {
namespace qt {
void CPUPane::Build() {
QWidget* widget = new QWidget();
widget->setStyleSheet("background: gray");
set_widget(widget);
}
} // namespace qt
} // namespace ui
} // namespace xe

View File

@ -0,0 +1,22 @@
#ifndef XENIA_UI_QT_CPU_PANE_H_
#define XENIA_UI_QT_CPU_PANE_H_
#include "settings_pane.h"
namespace xe {
namespace ui {
namespace qt {
class CPUPane : public SettingsPane {
Q_OBJECT
public:
explicit CPUPane() : SettingsPane(0xEC4A, "CPU") {}
void Build() override;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

View File

@ -0,0 +1,100 @@
#include "general_pane.h"
#include <QLabel>
#include <QVBoxLayout>
#include "xenia/ui/qt/widgets/checkbox.h"
#include "xenia/ui/qt/widgets/combobox.h"
#include "xenia/ui/qt/widgets/groupbox.h"
#include "xenia/ui/qt/widgets/scroll_area.h"
DECLARE_bool(show_debug_tab)
namespace xe {
namespace ui {
namespace qt {
const QStringList game_languages = {
"English", "Japanese", "German", "French", "Spanish",
"Italian", "Korean", "Chinese", "Portugese", "Polish",
"Russian", "Swedish", "Turkish", "Norwegian", "Dutch"};
void GeneralPane::Build() {
QWidget* base_widget = new QWidget();
base_widget->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::MinimumExpanding);
// Setup scroll area for settings pane
XScrollArea* scroll_area = new XScrollArea(this);
scroll_area->setWidget(base_widget);
scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scroll_area->setWidgetResizable(true);
QVBoxLayout* layout = new QVBoxLayout();
base_widget->setLayout(layout);
layout->setSpacing(16);
layout->setContentsMargins(32, 16, 32, 16);
// Add settings groupboxes to layout
layout->addWidget(CreateGeneralGroupBox());
layout->addWidget(CreateUpdateGroupBox());
layout->addWidget(CreateWindowGroupBox());
layout->addWidget(CreateLogGroupBox());
layout->addStretch();
set_widget(scroll_area);
}
XGroupBox* GeneralPane::CreateGeneralGroupBox() {
XGroupBox* groupbox = new XGroupBox("General Settings");
QVBoxLayout* groupbox_layout = new QVBoxLayout();
groupbox_layout->setContentsMargins(16, 16, 16, 16);
groupbox->setLayout(groupbox_layout);
XCheckBox* discord_presence_checkbox = new XCheckBox();
discord_presence_checkbox->setText("Discord Rich Presence");
connect(discord_presence_checkbox, &XCheckBox::stateChanged,
[&](bool value) {
//update_config_var(cvars::cv_show_debug_tab, value);
});
groupbox_layout->addWidget(discord_presence_checkbox);
XCheckBox* game_icon_checkbox = new XCheckBox();
game_icon_checkbox->setText("Show Game Icon in Taskbar");
groupbox_layout->addWidget(game_icon_checkbox);
QHBoxLayout* game_language_layout = new QHBoxLayout();
game_language_layout->setContentsMargins(0, 0, 0, 0);
game_language_layout->setSpacing(16);
QLabel* game_language_label = new QLabel("Game Language");
XComboBox* game_language_combobox = new XComboBox();
game_language_combobox->addItems(game_languages);
game_language_layout->addWidget(game_language_label);
game_language_layout->addWidget(game_language_combobox);
game_language_layout->addStretch();
groupbox_layout->addLayout(game_language_layout);
return groupbox;
}
XGroupBox* GeneralPane::CreateUpdateGroupBox() {
return new XGroupBox("Update Settings");
}
XGroupBox* GeneralPane::CreateWindowGroupBox() {
return new XGroupBox("Window Settings");
}
XGroupBox* GeneralPane::CreateLogGroupBox() {
return new XGroupBox("Log Settings");
}
} // namespace qt
} // namespace ui
} // namespace xe

View File

@ -0,0 +1,30 @@
#ifndef XENIA_UI_QT_GENERAL_PANE_H_
#define XENIA_UI_QT_GENERAL_PANE_H_
#include "settings_pane.h"
namespace xe {
namespace ui {
namespace qt {
class XGroupBox;
class GeneralPane : public SettingsPane {
Q_OBJECT
public:
explicit GeneralPane() : SettingsPane(0xE713, "General") {}
void Build() override;
private:
XGroupBox* CreateGeneralGroupBox();
XGroupBox* CreateUpdateGroupBox();
XGroupBox* CreateWindowGroupBox();
XGroupBox* CreateLogGroupBox();
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

View File

@ -0,0 +1,16 @@
#include "gpu_pane.h"
namespace xe {
namespace ui {
namespace qt {
void GPUPane::Build() {
QWidget* widget = new QWidget();
widget->setStyleSheet("background: orange");
set_widget(widget);
}
} // namespace qt
} // namespace ui
} // namespace xe

View File

@ -0,0 +1,22 @@
#ifndef XENIA_UI_QT_GPU_PANE_H_
#define XENIA_UI_QT_GPU_PANE_H_
#include "settings_pane.h"
namespace xe {
namespace ui {
namespace qt {
class GPUPane : public SettingsPane {
Q_OBJECT
public:
explicit GPUPane() : SettingsPane(0xE7F4, "GPU") {}
void Build() override;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

View File

@ -0,0 +1,16 @@
#include "interface_pane.h"
namespace xe {
namespace ui {
namespace qt {
void InterfacePane::Build() {
QWidget* widget = new QWidget();
widget->setStyleSheet("background: brown");
set_widget(widget);
}
} // namespace qt
} // namespace ui
} // namespace xe

View File

@ -0,0 +1,22 @@
#ifndef XENIA_UI_QT_INTERFACE_PANE_H_
#define XENIA_UI_QT_INTERFACE_PANE_H_
#include "settings_pane.h"
namespace xe {
namespace ui {
namespace qt {
class InterfacePane : public SettingsPane {
Q_OBJECT
public:
explicit InterfacePane() : SettingsPane(0xE790, "Interface") {}
void Build() override;
};
} // namespace qt
} // namespace ui
} // namespace xe
#endif

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