diff --git a/src/xenia/base/platform.h b/src/xenia/base/platform.h index a3ededa78..a2960bd47 100644 --- a/src/xenia/base/platform.h +++ b/src/xenia/base/platform.h @@ -58,6 +58,8 @@ #define strdup _strdup #define strcasecmp _stricmp #define strncasecmp _strnicmp +#undef DeleteBitmap +#undef GetFirstChild #endif // XE_PLATFORM_WIN32 #if XE_COMPILER_MSVC diff --git a/src/xenia/debug/ui/application.cc b/src/xenia/debug/ui/application.cc index 838c8764a..fe48ed30c 100644 --- a/src/xenia/debug/ui/application.cc +++ b/src/xenia/debug/ui/application.cc @@ -9,19 +9,33 @@ #include "xenia/debug/ui/application.h" +#include "xenia/base/assert.h" #include "xenia/base/logging.h" #include "xenia/base/platform.h" #include "xenia/base/threading.h" #include "xenia/debug/ui/main_window.h" #include "xenia/profiling.h" +#include "third_party/turbobadger/src/tb/tb_msg.h" +#include "third_party/turbobadger/src/tb/tb_system.h" + namespace xe { namespace debug { namespace ui { -Application::Application() {} +Application* current_application_ = nullptr; -Application::~Application() = default; +Application* Application::current() { + assert_not_null(current_application_); + return current_application_; +} + +Application::Application() { current_application_ = this; } + +Application::~Application() { + assert_true(current_application_ == this); + current_application_ = nullptr; +} std::unique_ptr Application::Create() { std::unique_ptr app(new Application()); @@ -64,3 +78,33 @@ void Application::Quit() { } // namespace ui } // namespace debug } // namespace xe + +// This doesn't really belong here (it belongs in tb_system_[linux/windows].cpp. +// This is here since the proper implementations has not yet been done. +void tb::TBSystem::RescheduleTimer(uint64_t fire_time) { + if (fire_time == tb::TB_NOT_SOON) { + return; + } + + uint64_t now = tb::TBSystem::GetTimeMS(); + uint64_t delay_millis = fire_time >= now ? fire_time - now : 0; + xe::debug::ui::Application::current()->loop()->PostDelayed([]() { + uint64_t next_fire_time = tb::TBMessageHandler::GetNextMessageFireTime(); + uint64_t now = tb::TBSystem::GetTimeMS(); + if (now < next_fire_time) { + // We timed out *before* we were supposed to (the OS is not playing nice). + // Calling ProcessMessages now won't achieve a thing so force a reschedule + // of the platform timer again with the same time. + // ReschedulePlatformTimer(next_fire_time, true); + return; + } + + tb::TBMessageHandler::ProcessMessages(); + + // If we still have things to do (because we didn't process all messages, + // or because there are new messages), we need to rescedule, so call + // RescheduleTimer. + tb::TBSystem::RescheduleTimer( + tb::TBMessageHandler::GetNextMessageFireTime()); + }, delay_millis); +} diff --git a/src/xenia/debug/ui/application.h b/src/xenia/debug/ui/application.h index c3e54ddcd..6ff3cd70b 100644 --- a/src/xenia/debug/ui/application.h +++ b/src/xenia/debug/ui/application.h @@ -25,6 +25,7 @@ class Application { ~Application(); static std::unique_ptr Create(); + static Application* current(); xe::ui::Loop* loop() { return &loop_; } MainWindow* main_window() const { return main_window_.get(); } diff --git a/src/xenia/debug/ui/main_window.cc b/src/xenia/debug/ui/main_window.cc index fa932a43f..147deca70 100644 --- a/src/xenia/debug/ui/main_window.cc +++ b/src/xenia/debug/ui/main_window.cc @@ -13,6 +13,7 @@ #include "xenia/base/logging.h" #include "xenia/base/platform.h" #include "xenia/base/threading.h" +#include "xenia/debug/ui/turbo_badger_control.h" namespace xe { namespace debug { @@ -88,7 +89,7 @@ bool MainWindow::Initialize() { // Setup the GL control that actually does the drawing. // We run here in the loop and only touch it (and its context) on this // thread. That means some sync-fu when we want to swap. - control_ = std::make_unique(loop()); + control_ = std::make_unique(loop()); AddChild(control_.get()); Resize(1440, 1200); diff --git a/src/xenia/debug/ui/main_window.h b/src/xenia/debug/ui/main_window.h index a7de99a25..cd82e495e 100644 --- a/src/xenia/debug/ui/main_window.h +++ b/src/xenia/debug/ui/main_window.h @@ -13,7 +13,6 @@ #include #include "xenia/debug/ui/application.h" -#include "xenia/ui/gl/wgl_control.h" #include "xenia/ui/platform.h" #include "xenia/ui/window.h" @@ -21,6 +20,8 @@ namespace xe { namespace debug { namespace ui { +class TurboBadgerControl; + class MainWindow : public xe::ui::PlatformWindow { public: MainWindow(Application* app); @@ -36,7 +37,7 @@ class MainWindow : public xe::ui::PlatformWindow { Application* app_ = nullptr; xe::ui::PlatformMenu main_menu_; - std::unique_ptr control_; + std::unique_ptr control_; }; } // namespace ui diff --git a/src/xenia/debug/ui/turbo_badger_control.cc b/src/xenia/debug/ui/turbo_badger_control.cc new file mode 100644 index 000000000..929e0ff45 --- /dev/null +++ b/src/xenia/debug/ui/turbo_badger_control.cc @@ -0,0 +1,471 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2015 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/debug/ui/turbo_badger_control.h" + +#include "xenia/base/clock.h" +#include "xenia/base/logging.h" +#include "xenia/debug/ui/turbo_badger_renderer.h" + +// TODO(benvanik): remove this. +#include "xenia/debug/ui/application.h" + +#include "third_party/turbobadger/src/tb/tb_core.h" +#include "third_party/turbobadger/src/tb/tb_font_renderer.h" +#include "third_party/turbobadger/src/tb/tb_message_window.h" +#include "third_party/turbobadger/src/tb/tb_widget_animation.h" + +namespace xe { +namespace debug { +namespace ui { + +constexpr bool kContinuousRepaint = true; + +// Enables long press behaviors (context menu, etc). +constexpr bool kTouch = false; + +constexpr uint64_t kDoubleClickDelayMillis = 600; +constexpr double kDoubleClickDistance = 5; + +constexpr int32_t kMouseWheelDetent = 120; + +class RootWidget : public tb::TBWidget { + public: + RootWidget(TurboBadgerControl* owner) : owner_(owner) {} + void OnInvalid() override { owner_->Invalidate(); } + + private: + TurboBadgerControl* owner_ = nullptr; +}; + +bool TurboBadgerControl::InitializeTurboBadger(tb::TBRenderer* renderer) { + static bool has_initialized = false; + if (has_initialized) { + return true; + } + has_initialized = true; + + if (!tb::tb_core_init( + renderer, + "third_party/turbobadger/resources/language/lng_en.tb.txt")) { + XELOGE("Failed to initialize turbobadger core"); + return false; + } + + // Load the default skin, and override skin that contains the graphics + // specific to the demo. + if (!tb::g_tb_skin->Load( + "third_party/turbobadger/resources/default_skin/skin.tb.txt", + "third_party/turbobadger/Demo/demo01/skin/skin.tb.txt")) { + XELOGE("Failed to load turbobadger skin"); + return false; + } + +// Register font renderers. +#ifdef TB_FONT_RENDERER_TBBF + void register_tbbf_font_renderer(); + register_tbbf_font_renderer(); +#endif +#ifdef TB_FONT_RENDERER_STB + void register_stb_font_renderer(); + register_stb_font_renderer(); +#endif +#ifdef TB_FONT_RENDERER_FREETYPE + void register_freetype_font_renderer(); + register_freetype_font_renderer(); +#endif + +// Add fonts we can use to the font manager. +#if defined(TB_FONT_RENDERER_STB) || defined(TB_FONT_RENDERER_FREETYPE) + tb::g_font_manager->AddFontInfo("third_party/turbobadger/resources/vera.ttf", + "Default"); +#endif +#ifdef TB_FONT_RENDERER_TBBF + tb::g_font_manager->AddFontInfo( + "third_party/turbobadger/resources/default_font/" + "segoe_white_with_shadow.tb.txt", + "Default"); +#endif + + // Set the default font description for widgets to one of the fonts we just + // added. + tb::TBFontDescription fd; + fd.SetID(TBIDC("Default")); + fd.SetSize(tb::g_tb_skin->GetDimensionConverter()->DpToPx(14)); + tb::g_font_manager->SetDefaultFontDescription(fd); + + // Create the font now. + auto font = tb::g_font_manager->CreateFontFace( + tb::g_font_manager->GetDefaultFontDescription()); + return true; +} + +void TurboBadgerControl::ShutdownTurboBadger() { tb::tb_core_shutdown(); } + +TurboBadgerControl::TurboBadgerControl(xe::ui::Loop* loop) : super(loop) {} + +TurboBadgerControl::~TurboBadgerControl() = default; + +bool TurboBadgerControl::Create() { + if (!super::Create()) { + return false; + } + + // TODO(benvanik): setup renderer? + renderer_ = TBRendererGL4::Create(); + + if (!InitializeTurboBadger(renderer_.get())) { + XELOGE("Unable to initialize turbobadger"); + return false; + } + + tb::TBWidgetsAnimationManager::Init(); + + // TODO(benvanik): setup widgets. + root_widget_ = std::make_unique(this); + root_widget_->SetSkinBg(TBIDC("background")); + root_widget_->SetRect(tb::TBRect(0, 0, 1000, 1000)); + + // Block animations during init. + tb::TBAnimationBlocker anim_blocker; + + // TODO(benvanik): dummy UI. + auto message_window = new tb::TBMessageWindow(root_widget(), TBIDC("")); + message_window->Show("Title", "Hello!"); + + // tb::ShowDebugInfoSettingsWindow(root_widget()); + + return true; +} + +void TurboBadgerControl::Destroy() { + tb::TBWidgetsAnimationManager::Shutdown(); + super::Destroy(); +} + +void TurboBadgerControl::OnLayout(xe::ui::UIEvent& e) { + super::OnLayout(e); + if (!root_widget()) { + return; + } + // TODO(benvanik): subregion? + root_widget()->SetRect(tb::TBRect(0, 0, width(), height())); +} + +void TurboBadgerControl::OnPaint(xe::ui::UIEvent& e) { + super::OnPaint(e); + + ++frame_count_; + ++fps_frame_count_; + uint64_t now_ns = xe::Clock::QueryHostSystemTime(); + if (now_ns > fps_update_time_ + 1000 * 10000) { + fps_ = uint32_t(fps_frame_count_ / + (double(now_ns - fps_update_time_) / 10000000.0)); + fps_update_time_ = now_ns; + fps_frame_count_ = 0; + } + + // Update TB (run animations, handle deferred input, etc). + tb::TBAnimationManager::Update(); + root_widget()->InvokeProcessStates(); + root_widget()->InvokeProcess(); + + renderer()->BeginPaint(width(), height()); + + // Render entire control hierarchy. + root_widget()->InvokePaint(tb::TBWidget::PaintProps()); + + // Render debug overlay. + root_widget()->GetFont()->DrawString( + 5, 5, tb::TBColor(255, 0, 0), + tb::format_string("Frame %lld", frame_count_)); + if (kContinuousRepaint) { + root_widget()->GetFont()->DrawString(5, 20, tb::TBColor(255, 0, 0), + tb::format_string("FPS: %d", fps_)); + } + + renderer()->EndPaint(); + + // If animations are running, reinvalidate immediately. + if (tb::TBAnimationManager::HasAnimationsRunning()) { + root_widget()->Invalidate(); + } + if (kContinuousRepaint) { + // Force an immediate repaint, always. + root_widget()->Invalidate(); + } +} + +void TurboBadgerControl::OnGotFocus(xe::ui::UIEvent& e) { + super::OnGotFocus(e); +} + +void TurboBadgerControl::OnLostFocus(xe::ui::UIEvent& e) { + super::OnLostFocus(e); + modifier_shift_pressed_ = false; + modifier_cntrl_pressed_ = false; + modifier_alt_pressed_ = false; + modifier_super_pressed_ = false; + last_click_time_ = 0; +} + +tb::MODIFIER_KEYS TurboBadgerControl::GetModifierKeys() { + auto modifiers = tb::TB_MODIFIER_NONE; + if (modifier_shift_pressed_) { + modifiers |= tb::MODIFIER_KEYS::TB_SHIFT; + } + if (modifier_cntrl_pressed_) { + modifiers |= tb::MODIFIER_KEYS::TB_CTRL; + } + if (modifier_alt_pressed_) { + modifiers |= tb::MODIFIER_KEYS::TB_ALT; + } + if (modifier_super_pressed_) { + modifiers |= tb::MODIFIER_KEYS::TB_SUPER; + } + return modifiers; +} + +void TurboBadgerControl::OnKeyPress(xe::ui::KeyEvent& e, bool is_down) { + tb::SPECIAL_KEY special_key = tb::SPECIAL_KEY::TB_KEY_UNDEFINED; + switch (e.key_code()) { + case 38: + special_key = tb::SPECIAL_KEY::TB_KEY_UP; + break; + case 39: + special_key = tb::SPECIAL_KEY::TB_KEY_RIGHT; + break; + case 40: + special_key = tb::SPECIAL_KEY::TB_KEY_DOWN; + break; + case 37: + special_key = tb::SPECIAL_KEY::TB_KEY_LEFT; + break; + case 112: + special_key = tb::SPECIAL_KEY::TB_KEY_F1; + break; + case 113: + special_key = tb::SPECIAL_KEY::TB_KEY_F2; + break; + case 114: + special_key = tb::SPECIAL_KEY::TB_KEY_F3; + break; + case 115: + special_key = tb::SPECIAL_KEY::TB_KEY_F4; + break; + case 116: + special_key = tb::SPECIAL_KEY::TB_KEY_F5; + break; + case 117: + special_key = tb::SPECIAL_KEY::TB_KEY_F6; + break; + case 118: + special_key = tb::SPECIAL_KEY::TB_KEY_F7; + break; + case 119: + special_key = tb::SPECIAL_KEY::TB_KEY_F8; + break; + case 120: + special_key = tb::SPECIAL_KEY::TB_KEY_F9; + break; + case 121: + special_key = tb::SPECIAL_KEY::TB_KEY_F10; + break; + case 122: + special_key = tb::SPECIAL_KEY::TB_KEY_F11; + break; + case 123: + special_key = tb::SPECIAL_KEY::TB_KEY_F12; + break; + case 33: + special_key = tb::SPECIAL_KEY::TB_KEY_PAGE_UP; + break; + case 34: + special_key = tb::SPECIAL_KEY::TB_KEY_PAGE_DOWN; + break; + case 36: + special_key = tb::SPECIAL_KEY::TB_KEY_HOME; + break; + case 35: + special_key = tb::SPECIAL_KEY::TB_KEY_END; + break; + case 45: + special_key = tb::SPECIAL_KEY::TB_KEY_INSERT; + break; + case 9: + special_key = tb::SPECIAL_KEY::TB_KEY_TAB; + break; + case 46: + special_key = tb::SPECIAL_KEY::TB_KEY_DELETE; + break; + case 8: + special_key = tb::SPECIAL_KEY::TB_KEY_BACKSPACE; + break; + case 13: + special_key = tb::SPECIAL_KEY::TB_KEY_ENTER; + break; + case 27: + special_key = tb::SPECIAL_KEY::TB_KEY_ESC; + break; + case 93: + if (!is_down && tb::TBWidget::focused_widget) { + tb::TBWidgetEvent ev(tb::EVENT_TYPE_CONTEXT_MENU); + ev.modifierkeys = GetModifierKeys(); + tb::TBWidget::focused_widget->InvokeEvent(ev); + e.set_handled(true); + return; + } + break; + case 16: + modifier_shift_pressed_ = is_down; + break; + case 17: + modifier_cntrl_pressed_ = is_down; + break; + // case xx: + // // alt ?? + // modifier_alt_pressed_ = is_down; + // break; + case 91: + modifier_super_pressed_ = is_down; + break; + } + + if (!CheckShortcutKey(e, special_key, is_down)) { + e.set_handled(root_widget()->InvokeKey( + special_key != tb::SPECIAL_KEY::TB_KEY_UNDEFINED ? e.key_code() : 0, + special_key, GetModifierKeys(), is_down)); + } +} + +bool TurboBadgerControl::CheckShortcutKey(xe::ui::KeyEvent& e, + tb::SPECIAL_KEY special_key, + bool is_down) { + bool shortcut_key = modifier_cntrl_pressed_; + if (!tb::TBWidget::focused_widget || !is_down || !shortcut_key) { + return false; + } + bool reverse_key = modifier_shift_pressed_; + int upper_key = e.key_code(); + if (upper_key >= 'a' && upper_key <= 'z') { + upper_key += 'A' - 'a'; + } + tb::TBID id; + if (upper_key == 'X') { + id = TBIDC("cut"); + } else if (upper_key == 'C' || special_key == tb::TB_KEY_INSERT) { + id = TBIDC("copy"); + } else if (upper_key == 'V' || + (special_key == tb::TB_KEY_INSERT && reverse_key)) { + id = TBIDC("paste"); + } else if (upper_key == 'A') { + id = TBIDC("selectall"); + } else if (upper_key == 'Z' || upper_key == 'Y') { + bool undo = upper_key == 'Z'; + if (reverse_key) { + undo = !undo; + } + id = undo ? TBIDC("undo") : TBIDC("redo"); + } else if (upper_key == 'N') { + id = TBIDC("new"); + } else if (upper_key == 'O') { + id = TBIDC("open"); + } else if (upper_key == 'S') { + id = TBIDC("save"); + } else if (upper_key == 'W') { + id = TBIDC("close"); + } else if (special_key == tb::TB_KEY_PAGE_UP) { + id = TBIDC("prev_doc"); + } else if (special_key == tb::TB_KEY_PAGE_DOWN) { + id = TBIDC("next_doc"); + } else { + return false; + } + + tb::TBWidgetEvent ev(tb::EVENT_TYPE_SHORTCUT); + ev.modifierkeys = GetModifierKeys(); + ev.ref_id = id; + if (!tb::TBWidget::focused_widget->InvokeEvent(ev)) { + return false; + } + e.set_handled(true); + return true; +} + +void TurboBadgerControl::OnKeyDown(xe::ui::KeyEvent& e) { + super::OnKeyDown(e); + OnKeyPress(e, true); +} + +void TurboBadgerControl::OnKeyUp(xe::ui::KeyEvent& e) { + super::OnKeyUp(e); + OnKeyPress(e, false); +} + +void TurboBadgerControl::OnMouseDown(xe::ui::MouseEvent& e) { + super::OnMouseDown(e); + // TODO(benvanik): more button types. + if (e.button() == xe::ui::MouseEvent::Button::kLeft) { + // Simulated click count support. + // TODO(benvanik): move into Control? + uint64_t now = xe::Clock::QueryHostUptimeMillis(); + if (now < last_click_time_ + kDoubleClickDelayMillis) { + double distance_moved = std::sqrt(std::pow(e.x() - last_click_x_, 2.0) + + std::pow(e.y() - last_click_y_, 2.0)); + if (distance_moved < kDoubleClickDistance) { + ++last_click_counter_; + } else { + last_click_counter_ = 1; + } + } else { + last_click_counter_ = 1; + } + last_click_x_ = e.x(); + last_click_y_ = e.y(); + last_click_time_ = now; + + e.set_handled(root_widget()->InvokePointerDown( + e.x(), e.y(), last_click_counter_, GetModifierKeys(), kTouch)); + } +} + +void TurboBadgerControl::OnMouseMove(xe::ui::MouseEvent& e) { + super::OnMouseMove(e); + root_widget()->InvokePointerMove(e.x(), e.y(), GetModifierKeys(), kTouch); + e.set_handled(true); +} + +void TurboBadgerControl::OnMouseUp(xe::ui::MouseEvent& e) { + super::OnMouseUp(e); + if (e.button() == xe::ui::MouseEvent::Button::kLeft) { + e.set_handled(root_widget()->InvokePointerUp(e.x(), e.y(), + GetModifierKeys(), kTouch)); + } else if (e.button() == xe::ui::MouseEvent::Button::kRight) { + root_widget()->InvokePointerMove(e.x(), e.y(), GetModifierKeys(), kTouch); + if (tb::TBWidget::hovered_widget) { + int x = e.x(); + int y = e.y(); + tb::TBWidget::hovered_widget->ConvertFromRoot(x, y); + tb::TBWidgetEvent ev(tb::EVENT_TYPE_CONTEXT_MENU, x, y, kTouch, + GetModifierKeys()); + tb::TBWidget::hovered_widget->InvokeEvent(ev); + } + e.set_handled(true); + } +} + +void TurboBadgerControl::OnMouseWheel(xe::ui::MouseEvent& e) { + super::OnMouseWheel(e); + e.set_handled(root_widget()->InvokeWheel( + e.x(), e.y(), e.dx(), -e.dy() / kMouseWheelDetent, GetModifierKeys())); +} + +} // namespace ui +} // namespace debug +} // namespace xe diff --git a/src/xenia/debug/ui/turbo_badger_control.h b/src/xenia/debug/ui/turbo_badger_control.h new file mode 100644 index 000000000..5081270a9 --- /dev/null +++ b/src/xenia/debug/ui/turbo_badger_control.h @@ -0,0 +1,83 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2015 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_DEBUG_UI_TURBO_BADGER_CONTROL_H_ +#define XENIA_DEBUG_UI_TURBO_BADGER_CONTROL_H_ + +#include + +#include "xenia/ui/gl/wgl_control.h" +#include "xenia/ui/loop.h" + +#include "third_party/turbobadger/src/tb/tb_core.h" +#include "third_party/turbobadger/src/tb/tb_widgets.h" + +namespace xe { +namespace debug { +namespace ui { + +class TurboBadgerControl : public xe::ui::gl::WGLControl { + public: + TurboBadgerControl(xe::ui::Loop* loop); + ~TurboBadgerControl() override; + + static bool InitializeTurboBadger(tb::TBRenderer* renderer); + static void ShutdownTurboBadger(); + + tb::TBRenderer* renderer() const { return renderer_.get(); } + tb::TBWidget* root_widget() const { return root_widget_.get(); } + + protected: + using super = xe::ui::gl::WGLControl; + + bool Create() override; + void Destroy() override; + + void OnLayout(xe::ui::UIEvent& e) override; + void OnPaint(xe::ui::UIEvent& e) override; + + void OnGotFocus(xe::ui::UIEvent& e) override; + void OnLostFocus(xe::ui::UIEvent& e) override; + + tb::MODIFIER_KEYS GetModifierKeys(); + + void OnKeyPress(xe::ui::KeyEvent& e, bool is_down); + bool CheckShortcutKey(xe::ui::KeyEvent& e, tb::SPECIAL_KEY special_key, + bool is_down); + void OnKeyDown(xe::ui::KeyEvent& e) override; + void OnKeyUp(xe::ui::KeyEvent& e) override; + + void OnMouseDown(xe::ui::MouseEvent& e) override; + void OnMouseMove(xe::ui::MouseEvent& e) override; + void OnMouseUp(xe::ui::MouseEvent& e) override; + void OnMouseWheel(xe::ui::MouseEvent& e) override; + + std::unique_ptr renderer_; + std::unique_ptr root_widget_; + + uint32_t frame_count_ = 0; + uint32_t fps_ = 0; + uint64_t fps_update_time_ = 0; + uint64_t fps_frame_count_ = 0; + + bool modifier_shift_pressed_ = false; + bool modifier_cntrl_pressed_ = false; + bool modifier_alt_pressed_ = false; + bool modifier_super_pressed_ = false; + uint64_t last_click_time_ = 0; + int last_click_x_ = 0; + int last_click_y_ = 0; + int last_click_counter_ = 0; +}; + +} // namespace ui +} // namespace debug +} // namespace xe + +#endif // XENIA_DEBUG_UI_TURBO_BADGER_CONTROL_H_ diff --git a/src/xenia/debug/ui/turbo_badger_renderer.cc b/src/xenia/debug/ui/turbo_badger_renderer.cc new file mode 100644 index 000000000..742e3ce95 --- /dev/null +++ b/src/xenia/debug/ui/turbo_badger_renderer.cc @@ -0,0 +1,297 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2015 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/debug/ui/turbo_badger_renderer.h" + +#include "xenia/base/assert.h" +#include "xenia/base/logging.h" + +#include "third_party/turbobadger/src/tb/tb_types.h" +#include "third_party/turbobadger/src/tb/tb_bitmap_fragment.h" +#include "third_party/turbobadger/src/tb/tb_system.h" + +namespace xe { +namespace debug { +namespace ui { + +using namespace tb; + +class TBRendererGL4::TBBitmapGL4 : public tb::TBBitmap { + public: + TBBitmapGL4(TBRendererGL4* renderer); + ~TBBitmapGL4(); + + bool Init(int width, int height, uint32_t* data); + int Width() override { return width_; } + int Height() override { return height_; } + void SetData(uint32_t* data) override; + + TBRendererGL4* renderer_ = nullptr; + int width_ = 0; + int height_ = 0; + GLuint handle_ = 0; + GLuint64 gpu_handle_ = 0; +}; + +TBRendererGL4::TBBitmapGL4::TBBitmapGL4(TBRendererGL4* renderer) + : renderer_(renderer) {} + +TBRendererGL4::TBBitmapGL4::~TBBitmapGL4() { + // Must flush and unbind before we delete the texture + renderer_->FlushBitmap(this); + glMakeTextureHandleNonResidentARB(gpu_handle_); + glDeleteTextures(1, &handle_); +} + +bool TBRendererGL4::TBBitmapGL4::Init(int width, int height, uint32_t* data) { + assert(width == TBGetNearestPowerOfTwo(width)); + assert(height == TBGetNearestPowerOfTwo(height)); + width_ = width; + height_ = height; + + glCreateTextures(GL_TEXTURE_2D, 1, &handle_); + glTextureParameteri(handle_, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTextureParameteri(handle_, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTextureParameteri(handle_, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTextureParameteri(handle_, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTextureStorage2D(handle_, 1, GL_RGBA8, width_, height_); + + gpu_handle_ = glGetTextureHandleARB(handle_); + glMakeTextureHandleResidentARB(gpu_handle_); + + SetData(data); + + return true; +} + +void TBRendererGL4::TBBitmapGL4::SetData(uint32_t* data) { + renderer_->FlushBitmap(this); + glTextureSubImage2D(handle_, 0, 0, 0, width_, height_, GL_RGBA, + GL_UNSIGNED_BYTE, data); +} + +TBRendererGL4::TBRendererGL4() + : vertex_buffer_(VERTEX_BATCH_SIZE * sizeof(Vertex)) {} + +TBRendererGL4::~TBRendererGL4() { + vertex_buffer_.Shutdown(); + glDeleteVertexArrays(1, &vao_); + glDeleteProgram(program_); +} + +std::unique_ptr TBRendererGL4::Create() { + auto renderer = std::make_unique(); + if (!renderer->Initialize()) { + XELOGE("Failed to initialize TurboBadger GL4 renderer"); + return nullptr; + } + return renderer; +} + +bool TBRendererGL4::Initialize() { + if (!vertex_buffer_.Initialize()) { + XELOGE("Failed to initialize circular buffer"); + return false; + } + + const std::string header = + "\n\ +#version 450 \n\ +#extension GL_ARB_bindless_texture : require \n\ +#extension GL_ARB_explicit_uniform_location : require \n\ +#extension GL_ARB_shading_language_420pack : require \n\ +precision highp float; \n\ +precision highp int; \n\ +layout(std140, column_major) uniform; \n\ +layout(std430, column_major) buffer; \n\ +"; + const std::string vertex_shader_source = header + + "\n\ +layout(location = 0) uniform mat4 projection_matrix; \n\ +layout(location = 0) in vec2 in_pos; \n\ +layout(location = 1) in vec4 in_color; \n\ +layout(location = 2) in vec2 in_uv; \n\ +layout(location = 0) out vec4 vtx_color; \n\ +layout(location = 1) out vec2 vtx_uv; \n\ +void main() { \n\ + gl_Position = projection_matrix * vec4(in_pos.xy, 0.0, 1.0); \n\ + vtx_color = in_color; \n\ + vtx_uv = in_uv; \n\ +} \n\ +"; + const std::string fragment_shader_source = header + + "\n\ +layout(location = 1, bindless_sampler) uniform sampler2D texture_sampler; \n\ +layout(location = 2) uniform float texture_mix; \n\ +layout(location = 0) in vec4 vtx_color; \n\ +layout(location = 1) in vec2 vtx_uv; \n\ +layout(location = 0) out vec4 oC; \n\ +void main() { \n\ + oC = vtx_color; \n\ + if (texture_mix > 0.0) { \n\ + vec4 color = texture(texture_sampler, vtx_uv); \n\ + oC *= color.rgba; \n\ + } \n\ +} \n\ +"; + + GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER); + const char* vertex_shader_source_ptr = vertex_shader_source.c_str(); + GLint vertex_shader_source_length = GLint(vertex_shader_source.size()); + glShaderSource(vertex_shader, 1, &vertex_shader_source_ptr, + &vertex_shader_source_length); + glCompileShader(vertex_shader); + + GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); + const char* fragment_shader_source_ptr = fragment_shader_source.c_str(); + GLint fragment_shader_source_length = GLint(fragment_shader_source.size()); + glShaderSource(fragment_shader, 1, &fragment_shader_source_ptr, + &fragment_shader_source_length); + glCompileShader(fragment_shader); + + program_ = glCreateProgram(); + glAttachShader(program_, vertex_shader); + glAttachShader(program_, fragment_shader); + glLinkProgram(program_); + glDeleteShader(vertex_shader); + glDeleteShader(fragment_shader); + + glCreateVertexArrays(1, &vao_); + glEnableVertexArrayAttrib(vao_, 0); + glVertexArrayAttribBinding(vao_, 0, 0); + glVertexArrayAttribFormat(vao_, 0, 2, GL_FLOAT, GL_FALSE, + offsetof(Vertex, x)); + glEnableVertexArrayAttrib(vao_, 1); + glVertexArrayAttribBinding(vao_, 1, 0); + glVertexArrayAttribFormat(vao_, 1, 4, GL_UNSIGNED_BYTE, GL_TRUE, + offsetof(Vertex, col)); + glEnableVertexArrayAttrib(vao_, 2); + glVertexArrayAttribBinding(vao_, 2, 0); + glVertexArrayAttribFormat(vao_, 2, 2, GL_FLOAT, GL_FALSE, + offsetof(Vertex, u)); + glVertexArrayVertexBuffer(vao_, 0, vertex_buffer_.handle(), 0, + sizeof(Vertex)); + + return true; +} + +TBBitmap* TBRendererGL4::CreateBitmap(int width, int height, uint32_t* data) { + auto bitmap = std::make_unique(this); + if (!bitmap->Init(width, height, data)) { + return nullptr; + } + return bitmap.release(); +} + +void TBRendererGL4::SetClipRect(const TBRect& rect) { + Flush(); + glScissor(m_clip_rect.x, m_screen_rect.h - (m_clip_rect.y + m_clip_rect.h), + m_clip_rect.w, m_clip_rect.h); +} + +void TBRendererGL4::BeginPaint(int render_target_w, int render_target_h) { + TBRendererBatcher::BeginPaint(render_target_w, render_target_h); + + glEnablei(GL_BLEND, 0); + glBlendFunci(0, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + glEnable(GL_SCISSOR_TEST); + + glViewport(0, 0, render_target_w, render_target_h); + glScissor(0, 0, render_target_w, render_target_h); + + float left = 0.0f; + float right = float(render_target_w); + float bottom = float(render_target_h); + float top = 0.0f; + float z_near = -1.0f; + float z_far = 1.0f; + float projection[16] = {0}; + projection[0] = 2.0f / (right - left); + projection[5] = 2.0f / (top - bottom); + projection[10] = -2.0f / (z_far - z_near); + projection[12] = -(right + left) / (right - left); + projection[13] = -(top + bottom) / (top - bottom); + projection[14] = -(z_far + z_near) / (z_far - z_near); + projection[15] = 1.0f; + glProgramUniformMatrix4fv(program_, 0, 1, GL_FALSE, projection); + + current_bitmap_ = nullptr; + + glUseProgram(program_); + glBindVertexArray(vao_); +} + +void TBRendererGL4::EndPaint() { + TBRendererBatcher::EndPaint(); + + Flush(); + + glUseProgram(0); + glBindVertexArray(0); +} + +void TBRendererGL4::Flush() { + if (!draw_command_count_) { + return; + } + vertex_buffer_.Flush(); + for (size_t i = 0; i < draw_command_count_; ++i) { + glDrawArrays(draw_commands_[i].prim_type, + GLint(draw_commands_[i].vertex_offset), + GLsizei(draw_commands_[i].vertex_count)); + } + draw_command_count_ = 0; + // TODO(benvanik): don't finish here. + vertex_buffer_.WaitUntilClean(); +} + +void TBRendererGL4::RenderBatch(Batch* batch) { + auto bitmap = static_cast(batch->bitmap); + if (bitmap != current_bitmap_) { + current_bitmap_ = bitmap; + Flush(); + glProgramUniformHandleui64ARB(program_, 1, + bitmap ? bitmap->gpu_handle_ : 0); + glProgramUniform1f(program_, 2, bitmap ? 1.0f : 0.0f); + } + + if (draw_command_count_ + 1 > kMaxCommands) { + Flush(); + } + size_t total_length = sizeof(Vertex) * batch->vertex_count; + if (!vertex_buffer_.CanAcquire(total_length)) { + Flush(); + } + auto allocation = vertex_buffer_.Acquire(total_length); + + // TODO(benvanik): custom batcher that lets us use the ringbuffer memory + // without a copy. + std::memcpy(allocation.host_ptr, batch->vertex, total_length); + + if (draw_command_count_ && + draw_commands_[draw_command_count_ - 1].prim_type == GL_TRIANGLES) { + // Coalesce. + assert_always("haven't seen this yet"); + auto& prev_command = draw_commands_[draw_command_count_ - 1]; + prev_command.vertex_count += batch->vertex_count; + } else { + auto& command = draw_commands_[draw_command_count_++]; + command.prim_type = GL_TRIANGLES; + command.vertex_offset = allocation.offset / sizeof(Vertex); + command.vertex_count = batch->vertex_count; + } + + vertex_buffer_.Commit(std::move(allocation)); +} + +} // namespace ui +} // namespace debug +} // namespace xe diff --git a/src/xenia/debug/ui/turbo_badger_renderer.h b/src/xenia/debug/ui/turbo_badger_renderer.h new file mode 100644 index 000000000..76c1a78e7 --- /dev/null +++ b/src/xenia/debug/ui/turbo_badger_renderer.h @@ -0,0 +1,64 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2015 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_DEBUG_UI_TURBO_BADGER_RENDERER_H_ +#define XENIA_DEBUG_UI_TURBO_BADGER_RENDERER_H_ + +#include + +#include "xenia/ui/gl/circular_buffer.h" +#include "xenia/ui/gl/gl.h" + +#include "third_party/turbobadger/src/tb/tb_renderer.h" +#include "third_party/turbobadger/src/tb/tb_renderer_batcher.h" + +namespace xe { +namespace debug { +namespace ui { + +class TBRendererGL4 : public tb::TBRendererBatcher { + public: + TBRendererGL4(); + ~TBRendererGL4() override; + + static std::unique_ptr Create(); + + void BeginPaint(int render_target_w, int render_target_h) override; + void EndPaint() override; + + tb::TBBitmap* CreateBitmap(int width, int height, uint32_t* data) override; + + void RenderBatch(Batch* batch) override; + void SetClipRect(const tb::TBRect& rect) override; + + private: + class TBBitmapGL4; + + bool Initialize(); + void Flush(); + + GLuint program_ = 0; + GLuint vao_ = 0; + xe::ui::gl::CircularBuffer vertex_buffer_; + + static const size_t kMaxCommands = 512; + struct { + GLenum prim_type; + size_t vertex_offset; + size_t vertex_count; + } draw_commands_[kMaxCommands] = {0}; + uint32_t draw_command_count_ = 0; + TBBitmapGL4* current_bitmap_ = nullptr; +}; + +} // namespace ui +} // namespace debug +} // namespace xe + +#endif // XENIA_DEBUG_UI_TURBO_BADGER_RENDERER_H_ diff --git a/src/xenia/debug/ui/xe-debug-ui.vcxproj b/src/xenia/debug/ui/xe-debug-ui.vcxproj index ebac6c8f5..43210310c 100644 --- a/src/xenia/debug/ui/xe-debug-ui.vcxproj +++ b/src/xenia/debug/ui/xe-debug-ui.vcxproj @@ -77,11 +77,15 @@ + + + + diff --git a/src/xenia/debug/ui/xe-debug-ui.vcxproj.filters b/src/xenia/debug/ui/xe-debug-ui.vcxproj.filters index b9a219804..13e4aef1f 100644 --- a/src/xenia/debug/ui/xe-debug-ui.vcxproj.filters +++ b/src/xenia/debug/ui/xe-debug-ui.vcxproj.filters @@ -27,6 +27,12 @@ src\xenia\debug\ui + + src\xenia\debug\ui + + + src\xenia\debug\ui + @@ -41,5 +47,11 @@ src\xenia\debug\ui + + src\xenia\debug\ui + + + src\xenia\debug\ui + \ No newline at end of file diff --git a/src/xenia/ui/gl/wgl_control.cc b/src/xenia/ui/gl/wgl_control.cc index 9c1e61733..86abb78e4 100644 --- a/src/xenia/ui/gl/wgl_control.cc +++ b/src/xenia/ui/gl/wgl_control.cc @@ -77,10 +77,11 @@ LRESULT WGLControl::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_PAINT: { + invalidated_ = false; + ValidateRect(hWnd, nullptr); SCOPE_profile_cpu_i("gpu", "xe::gpu::gl4::WGLControl::WM_PAINT"); { GLContextLock context_lock(&context_); - wglSwapIntervalEXT(0); float clear_color[] = {rand() / (float)RAND_MAX, 1.0f, 0, 1.0f}; glClearNamedFramebufferfv(0, GL_COLOR, 0, clear_color); @@ -100,6 +101,7 @@ LRESULT WGLControl::WndProc(HWND hWnd, UINT message, WPARAM wParam, SCOPE_profile_cpu_i("gpu", "xe::gpu::gl4::WGLControl::SwapBuffers"); SwapBuffers(context_.dc()); } + return 0; } break; } return Win32Control::WndProc(hWnd, message, wParam, lParam); diff --git a/src/xenia/ui/loop.h b/src/xenia/ui/loop.h index 7cfe4bfa2..272202c34 100644 --- a/src/xenia/ui/loop.h +++ b/src/xenia/ui/loop.h @@ -21,6 +21,7 @@ class Loop { virtual ~Loop() = default; virtual void Post(std::function fn) = 0; + virtual void PostDelayed(std::function fn, uint64_t delay_millis) = 0; virtual void Quit() = 0; virtual void AwaitQuit() = 0; diff --git a/src/xenia/ui/win32/win32_control.cc b/src/xenia/ui/win32/win32_control.cc index e67644fc2..776c9aa96 100644 --- a/src/xenia/ui/win32/win32_control.cc +++ b/src/xenia/ui/win32/win32_control.cc @@ -121,10 +121,13 @@ void Win32Control::OnResize(UIEvent& e) { } void Win32Control::Invalidate() { + if (invalidated_) { + return; + } + invalidated_ = true; InvalidateRect(hwnd_, nullptr, FALSE); for (auto& child_control : children_) { - auto win32_control = static_cast(child_control.get()); - win32_control->Invalidate(); + child_control->Invalidate(); } } @@ -234,8 +237,15 @@ LRESULT Win32Control::WndProc(HWND hWnd, UINT message, WPARAM wParam, break; } - case WM_PAINT: + case WM_PAINT: { + if (flags_ & kFlagOwnPaint) { + return 0; // ignore + } + invalidated_ = false; + auto e = UIEvent(this); + OnPaint(e); break; + } case WM_ERASEBKGND: if (flags_ & kFlagOwnPaint) { return 0; // ignore diff --git a/src/xenia/ui/win32/win32_control.h b/src/xenia/ui/win32/win32_control.h index 87e3d849d..b99fa7704 100644 --- a/src/xenia/ui/win32/win32_control.h +++ b/src/xenia/ui/win32/win32_control.h @@ -54,7 +54,8 @@ class Win32Control : public Control { virtual LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); - HWND hwnd_; + HWND hwnd_ = nullptr; + bool invalidated_ = true; private: bool HandleMouse(UINT message, WPARAM wParam, LPARAM lParam); diff --git a/src/xenia/ui/win32/win32_loop.cc b/src/xenia/ui/win32/win32_loop.cc index 38f325500..6ae28b368 100644 --- a/src/xenia/ui/win32/win32_loop.cc +++ b/src/xenia/ui/win32/win32_loop.cc @@ -28,6 +28,8 @@ class PostedFn { }; Win32Loop::Win32Loop() : thread_id_(0) { + timer_queue_ = CreateTimerQueue(); + xe::threading::Fence init_fence; thread_ = std::thread([&init_fence, this]() { xe::threading::set_name("Win32 Loop"); @@ -49,6 +51,14 @@ Win32Loop::Win32Loop() : thread_id_(0) { Win32Loop::~Win32Loop() { Quit(); thread_.join(); + + DeleteTimerQueueEx(timer_queue_, INVALID_HANDLE_VALUE); + std::lock_guard lock(pending_timers_mutex_); + while (!pending_timers_.empty()) { + auto timer = pending_timers_.back(); + pending_timers_.pop_back(); + delete timer; + } } void Win32Loop::ThreadMain() { @@ -82,6 +92,38 @@ void Win32Loop::Post(std::function fn) { } } +void Win32Loop::TimerQueueCallback(void* context, uint8_t) { + auto timer = reinterpret_cast(context); + auto loop = timer->loop; + auto fn = std::move(timer->fn); + DeleteTimerQueueTimer(timer->timer_queue, timer->timer_handle, NULL); + { + std::lock_guard lock(loop->pending_timers_mutex_); + loop->pending_timers_.remove(timer); + } + delete timer; + loop->Post(std::move(fn)); +} + +void Win32Loop::PostDelayed(std::function fn, uint64_t delay_millis) { + if (!delay_millis) { + Post(std::move(fn)); + return; + } + auto timer = new PendingTimer(); + timer->loop = this; + timer->timer_queue = timer_queue_; + timer->fn = std::move(fn); + { + std::lock_guard lock(pending_timers_mutex_); + pending_timers_.push_back(timer); + } + CreateTimerQueueTimer(&timer->timer_handle, timer_queue_, + WAITORTIMERCALLBACK(TimerQueueCallback), timer, + DWORD(delay_millis), 0, + WT_EXECUTEINTIMERTHREAD | WT_EXECUTEONLYONCE); +} + void Win32Loop::Quit() { assert_true(thread_id_ != 0); PostThreadMessage(thread_id_, kWmWin32LoopQuit, diff --git a/src/xenia/ui/win32/win32_loop.h b/src/xenia/ui/win32/win32_loop.h index 617d6b63f..e0abb5913 100644 --- a/src/xenia/ui/win32/win32_loop.h +++ b/src/xenia/ui/win32/win32_loop.h @@ -10,11 +10,11 @@ #ifndef XENIA_UI_WIN32_WIN32_LOOP_H_ #define XENIA_UI_WIN32_WIN32_LOOP_H_ -#include -#include #include +#include #include +#include "xenia/base/mutex.h" #include "xenia/base/platform.h" #include "xenia/base/threading.h" #include "xenia/ui/loop.h" @@ -29,16 +29,30 @@ class Win32Loop : public Loop { ~Win32Loop() override; void Post(std::function fn) override; + void PostDelayed(std::function fn, uint64_t delay_millis) override; void Quit() override; void AwaitQuit() override; private: + struct PendingTimer { + Win32Loop* loop; + HANDLE timer_queue; + HANDLE timer_handle; + std::function fn; + }; + void ThreadMain(); + static void TimerQueueCallback(void* context, uint8_t); + std::thread thread_; DWORD thread_id_; xe::threading::Fence quit_fence_; + + HANDLE timer_queue_; + xe::mutex pending_timers_mutex_; + std::list pending_timers_; }; } // namespace win32 diff --git a/third_party/turbobadger b/third_party/turbobadger index 654b731db..b5f4e50b9 160000 --- a/third_party/turbobadger +++ b/third_party/turbobadger @@ -1 +1 @@ -Subproject commit 654b731db496c6014daf48384e74f517fe6905ac +Subproject commit b5f4e50b9d8626a27ec111cc00fef1438062fbb8 diff --git a/xenia.sln b/xenia.sln index 98467362e..5729c0965 100644 --- a/xenia.sln +++ b/xenia.sln @@ -126,11 +126,17 @@ Global {21DDCB81-68A3-4AB2-8CB0-C2B051B9FDDC}.Release|x64.ActiveCfg = Release|x64 {21DDCB81-68A3-4AB2-8CB0-C2B051B9FDDC}.Release|x64.Build.0 = Release|x64 {156102D7-F2DD-4618-B2EB-2DFE607EE6DD}.Checked|x64.ActiveCfg = Debug|x64 + {156102D7-F2DD-4618-B2EB-2DFE607EE6DD}.Checked|x64.Build.0 = Debug|x64 {156102D7-F2DD-4618-B2EB-2DFE607EE6DD}.Debug|x64.ActiveCfg = Debug|x64 + {156102D7-F2DD-4618-B2EB-2DFE607EE6DD}.Debug|x64.Build.0 = Debug|x64 {156102D7-F2DD-4618-B2EB-2DFE607EE6DD}.Release|x64.ActiveCfg = Release|x64 + {156102D7-F2DD-4618-B2EB-2DFE607EE6DD}.Release|x64.Build.0 = Release|x64 {C5BA52F0-C86B-4817-921C-CCA257FC04BE}.Checked|x64.ActiveCfg = Release|x64 + {C5BA52F0-C86B-4817-921C-CCA257FC04BE}.Checked|x64.Build.0 = Release|x64 {C5BA52F0-C86B-4817-921C-CCA257FC04BE}.Debug|x64.ActiveCfg = Debug|x64 + {C5BA52F0-C86B-4817-921C-CCA257FC04BE}.Debug|x64.Build.0 = Debug|x64 {C5BA52F0-C86B-4817-921C-CCA257FC04BE}.Release|x64.ActiveCfg = Release|x64 + {C5BA52F0-C86B-4817-921C-CCA257FC04BE}.Release|x64.Build.0 = Release|x64 {93533067-6449-4691-88A8-026EBCFDCA97}.Checked|x64.ActiveCfg = Checked|x64 {93533067-6449-4691-88A8-026EBCFDCA97}.Checked|x64.Build.0 = Checked|x64 {93533067-6449-4691-88A8-026EBCFDCA97}.Debug|x64.ActiveCfg = Debug|x64