diff --git a/src/xenia/base/string.cc b/src/xenia/base/string.cc index e6cf5b651..760527477 100644 --- a/src/xenia/base/string.cc +++ b/src/xenia/base/string.cc @@ -63,6 +63,29 @@ std::string format_string(const char* format, va_list args) { } } +std::wstring format_string(const wchar_t* format, va_list args) { + if (!format) { + return L""; + } + size_t max_len = 64; + std::wstring new_s; + while (true) { + new_s.resize(max_len); + int ret = std::vswprintf(const_cast(new_s.data()), max_len, + format, args); + if (ret > max_len) { + // Needed size is known (+2 for termination and avoid ambiguity). + max_len = ret + 2; + } else if (ret == -1 || ret >= max_len - 1) { + // Handle some buggy vsnprintf implementations. + max_len *= 2; + } else { + // Everything fit for sure. + return new_s; + } + } +} + std::string::size_type find_first_of_case(const std::string& target, const std::string& search) { const char* str = target.c_str(); diff --git a/src/xenia/base/string.h b/src/xenia/base/string.h index 9cab4645f..0d8eef2c5 100644 --- a/src/xenia/base/string.h +++ b/src/xenia/base/string.h @@ -31,6 +31,14 @@ inline std::string format_string(const char* format, ...) { va_end(va); return result; } +std::wstring format_string(const wchar_t* format, va_list args); +inline std::wstring format_string(const wchar_t* format, ...) { + va_list va; + va_start(va, format); + auto result = format_string(format, va); + va_end(va); + return result; +} // find_first_of string, case insensitive. std::string::size_type find_first_of_case(const std::string& target, diff --git a/src/xenia/gpu/gl4/gl4_trace_dump_main.cc b/src/xenia/gpu/gl4/gl4_trace_dump_main.cc new file mode 100644 index 000000000..9928510f2 --- /dev/null +++ b/src/xenia/gpu/gl4/gl4_trace_dump_main.cc @@ -0,0 +1,72 @@ +/** + ****************************************************************************** + * 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/base/logging.h" +#include "xenia/base/main.h" +#include "xenia/gpu/gl4/gl4_command_processor.h" +#include "xenia/gpu/gl4/gl4_graphics_system.h" +#include "xenia/gpu/trace_dump.h" + +namespace xe { +namespace gpu { +namespace gl4 { + +using namespace xe::gpu::xenos; + +class GL4TraceDump : public TraceDump { + public: + std::unique_ptr CreateGraphicsSystem() override { + return std::unique_ptr(new GL4GraphicsSystem()); + } + + uintptr_t GetColorRenderTarget(uint32_t pitch, MsaaSamples samples, + uint32_t base, + ColorRenderTargetFormat format) override { + auto command_processor = static_cast( + graphics_system_->command_processor()); + return command_processor->GetColorRenderTarget(pitch, samples, base, + format); + } + + uintptr_t GetDepthRenderTarget(uint32_t pitch, MsaaSamples samples, + uint32_t base, + DepthRenderTargetFormat format) override { + auto command_processor = static_cast( + graphics_system_->command_processor()); + return command_processor->GetDepthRenderTarget(pitch, samples, base, + format); + } + + uintptr_t GetTextureEntry(const TextureInfo& texture_info, + const SamplerInfo& sampler_info) override { + auto command_processor = static_cast( + graphics_system_->command_processor()); + + auto entry_view = + command_processor->texture_cache()->Demand(texture_info, sampler_info); + if (!entry_view) { + return 0; + } + auto texture = entry_view->texture; + return static_cast(texture->handle); + } +}; + +int trace_dump_main(const std::vector& args) { + GL4TraceDump trace_dump; + return trace_dump.Main(args); +} + +} // namespace gl4 +} // namespace gpu +} // namespace xe + +DEFINE_ENTRY_POINT(L"xenia-gpu-gl4-trace-dump", + L"xenia-gpu-gl4-trace-dump some.trace", + xe::gpu::gl4::trace_dump_main); diff --git a/src/xenia/gpu/gl4/premake5.lua b/src/xenia/gpu/gl4/premake5.lua index e1145d266..2498a0fe1 100644 --- a/src/xenia/gpu/gl4/premake5.lua +++ b/src/xenia/gpu/gl4/premake5.lua @@ -81,3 +81,56 @@ project("xenia-gpu-gl4-trace-viewer") "1>scratch/stdout-trace-viewer.txt", }) end + +group("src") +project("xenia-gpu-gl4-trace-dump") + uuid("5d47299d-f37d-46b4-af48-f4e54b9e5662") + kind("ConsoleApp") + language("C++") + links({ + "elemental-forms", + "gflags", + "glew", + "imgui", + "xenia-apu", + "xenia-apu-nop", + "xenia-apu-xaudio2", + "xenia-base", + "xenia-core", + "xenia-cpu", + "xenia-cpu-backend-x64", + "xenia-debug", + "xenia-gpu", + "xenia-gpu-gl4", + "xenia-hid-nop", + "xenia-hid-winkey", + "xenia-hid-xinput", + "xenia-kernel", + "xenia-ui", + "xenia-ui-gl", + "xenia-vfs", + }) + defines({ + "GLEW_STATIC=1", + "GLEW_MX=1", + }) + includedirs({ + project_root.."/third_party/elemental-forms/src", + project_root.."/build_tools/third_party/gflags/src", + }) + files({ + "gl4_trace_dump_main.cc", + "../../base/main_"..platform_suffix..".cc", + }) + + filter("platforms:Windows") + -- Only create the .user file if it doesn't already exist. + local user_file = project_root.."/build/xenia-gpu-gl4-trace-dump.vcxproj.user" + if not os.isfile(user_file) then + debugdir(project_root) + debugargs({ + "--flagfile=scratch/flags.txt", + "2>&1", + "1>scratch/stdout-trace-dump.txt", + }) + end diff --git a/src/xenia/gpu/trace_dump.cc b/src/xenia/gpu/trace_dump.cc new file mode 100644 index 000000000..63d3ac34f --- /dev/null +++ b/src/xenia/gpu/trace_dump.cc @@ -0,0 +1,219 @@ +/** + ****************************************************************************** + * 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/gpu/trace_dump.h" + +#include + +#include "third_party/stb/stb_image_write.h" +#include "xenia/base/logging.h" +#include "xenia/base/string.h" +#include "xenia/base/threading.h" +#include "xenia/gpu/command_processor.h" +#include "xenia/gpu/graphics_system.h" +#include "xenia/memory.h" +#include "xenia/ui/file_picker.h" +#include "xenia/ui/window.h" +#include "xenia/xbox.h" + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#undef _CRT_SECURE_NO_WARNINGS +#undef _CRT_NONSTDC_NO_DEPRECATE +#include "third_party/stb/stb_image_write.h" + +DEFINE_string(target_trace_file, "", "Specifies the trace file to load."); +DEFINE_string(trace_dump_path, "", "Output path for dumped files."); + +namespace xe { +namespace gpu { + +using namespace xe::gpu::xenos; + +TraceDump::TraceDump() = default; + +TraceDump::~TraceDump() = default; + +int TraceDump::Main(const std::vector& args) { + // Grab path from the flag or unnamed argument. + std::wstring path; + if (!FLAGS_target_trace_file.empty()) { + // Passed as a named argument. + // TODO(benvanik): find something better than gflags that supports + // unicode. + path = xe::to_wstring(FLAGS_target_trace_file); + } else if (args.size() >= 2) { + // Passed as an unnamed argument. + path = args[1]; + } + + // If no path passed, ask the user. + if (path.empty()) { + 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(L"Select Trace File"); + file_picker->set_extensions({ + {L"Supported Files", L"*.*"}, {L"All Files (*.*)", L"*.*"}, + }); + if (file_picker->Show()) { + auto selected_files = file_picker->selected_files(); + if (!selected_files.empty()) { + path = selected_files[0]; + } + } + } + + if (path.empty()) { + xe::FatalError("No trace file specified"); + return 1; + } + + // Normalize the path and make absolute. + auto abs_path = xe::to_absolute_path(path); + + if (!Setup()) { + xe::FatalError("Unable to setup trace dump tool"); + return 1; + } + if (!Load(std::move(abs_path))) { + xe::FatalError("Unable to load trace file; not found?"); + return 1; + } + + // Root file name for outputs. + base_output_path_ = + xe::fix_path_separators(xe::to_wstring(FLAGS_trace_dump_path)); + base_output_path_ = + xe::join_paths(base_output_path_, + xe::find_name_from_path(xe::fix_path_separators(path))); + + // Ensure output path exists. + xe::filesystem::CreateParentFolder(base_output_path_); + + Run(); + return 0; +} + +bool TraceDump::Setup() { + // Main display window. + loop_ = ui::Loop::Create(); + window_ = xe::ui::Window::Create(loop_.get(), L"xenia-gpu-trace-dump"); + loop_->PostSynchronous([&]() { + xe::threading::set_name("Win32 Loop"); + if (!window_->Initialize()) { + xe::FatalError("Failed to initialize main window"); + return; + } + }); + window_->on_closed.AddListener([&](xe::ui::UIEvent* e) { + loop_->Quit(); + XELOGI("User-initiated death!"); + exit(1); + }); + loop_->on_quit.AddListener([&](xe::ui::UIEvent* e) { window_.reset(); }); + window_->Resize(1920, 1200); + + // Create the emulator but don't initialize so we can setup the window. + emulator_ = std::make_unique(L""); + X_STATUS result = + emulator_->Setup(window_.get(), nullptr, + [this]() { return CreateGraphicsSystem(); }, nullptr); + if (XFAILED(result)) { + XELOGE("Failed to setup emulator: %.8X", result); + return false; + } + memory_ = emulator_->memory(); + graphics_system_ = emulator_->graphics_system(); + + window_->on_key_char.AddListener([&](xe::ui::KeyEvent* e) { + if (e->key_code() == 0x74 /* VK_F5 */) { + graphics_system_->ClearCaches(); + e->set_handled(true); + } + }); + + player_ = std::make_unique(loop_.get(), graphics_system_); + + window_->on_painting.AddListener([&](xe::ui::UIEvent* e) { + // Update titlebar status. + auto file_name = xe::find_name_from_path(trace_file_path_); + std::wstring title = std::wstring(L"Xenia GPU Trace Dump: ") + file_name; + if (player_->is_playing_trace()) { + int percent = + static_cast(player_->playback_percent() / 10000.0 * 100.0); + title += xe::format_string(L" (%d%%)", percent); + } + window_->set_title(title); + + // Continuous paint. + window_->Invalidate(); + }); + window_->Invalidate(); + + return true; +} + +bool TraceDump::Load(std::wstring trace_file_path) { + trace_file_path_ = std::move(trace_file_path); + + if (!player_->Open(trace_file_path_)) { + XELOGE("Could not load trace file"); + return false; + } + + return true; +} + +void TraceDump::Run() { + loop_->Post([&]() { + player_->SeekFrame(0); + player_->SeekCommand( + static_cast(player_->current_frame()->commands.size() - 1)); + }); + player_->WaitOnPlayback(); + + loop_->PostSynchronous([&]() { + // Breathe. + }); + loop_->PostSynchronous([&]() { + // Breathe. + }); + + xe::threading::Fence capture_fence; + loop_->PostDelayed( + [&]() { + // Capture. + auto raw_image = window_->context()->Capture(); + + // Save framebuffer png. + std::string png_path = xe::to_string(base_output_path_ + L".png"); + stbi_write_png(png_path.c_str(), static_cast(raw_image->width), + static_cast(raw_image->height), 4, + raw_image->data.data(), + static_cast(raw_image->stride)); + + capture_fence.Signal(); + }, + 50); + + capture_fence.Wait(); + + // Wait until we are exited. + loop_->Quit(); + loop_->AwaitQuit(); + + player_.reset(); + emulator_.reset(); + window_.reset(); + loop_.reset(); +} + +} // namespace gpu +} // namespace xe diff --git a/src/xenia/gpu/trace_dump.h b/src/xenia/gpu/trace_dump.h new file mode 100644 index 000000000..ac107d70b --- /dev/null +++ b/src/xenia/gpu/trace_dump.h @@ -0,0 +1,74 @@ +/** + ****************************************************************************** + * 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_GPU_TRACE_DUMP_H_ +#define XENIA_GPU_TRACE_DUMP_H_ + +#include + +#include "xenia/emulator.h" +#include "xenia/gpu/shader.h" +#include "xenia/gpu/trace_player.h" +#include "xenia/gpu/trace_protocol.h" +#include "xenia/gpu/xenos.h" +#include "xenia/memory.h" + +namespace xe { +namespace ui { +class Loop; +class Window; +} // namespace ui +} // namespace xe + +namespace xe { +namespace gpu { + +struct SamplerInfo; +struct TextureInfo; + +class TraceDump { + public: + virtual ~TraceDump(); + + int Main(const std::vector& args); + + protected: + TraceDump(); + + virtual std::unique_ptr CreateGraphicsSystem() = 0; + + virtual uintptr_t GetColorRenderTarget(uint32_t pitch, MsaaSamples samples, + uint32_t base, + ColorRenderTargetFormat format) = 0; + virtual uintptr_t GetDepthRenderTarget(uint32_t pitch, MsaaSamples samples, + uint32_t base, + DepthRenderTargetFormat format) = 0; + virtual uintptr_t GetTextureEntry(const TextureInfo& texture_info, + const SamplerInfo& sampler_info) = 0; + + std::unique_ptr loop_; + std::unique_ptr window_; + std::unique_ptr emulator_; + Memory* memory_ = nullptr; + GraphicsSystem* graphics_system_ = nullptr; + std::unique_ptr player_; + + private: + bool Setup(); + bool Load(std::wstring trace_file_path); + void Run(); + + std::wstring trace_file_path_; + std::wstring base_output_path_; +}; + +} // namespace gpu +} // namespace xe + +#endif // XENIA_GPU_TRACE_DUMP_H_ diff --git a/src/xenia/gpu/trace_player.cc b/src/xenia/gpu/trace_player.cc index 3e762f451..aea69e119 100644 --- a/src/xenia/gpu/trace_player.cc +++ b/src/xenia/gpu/trace_player.cc @@ -28,6 +28,8 @@ TracePlayer::TracePlayer(xe::ui::Loop* loop, GraphicsSystem* graphics_system) ->AllocFixed(0, 0x1FFFFFFF, 4096, kMemoryAllocationReserve | kMemoryAllocationCommit, kMemoryProtectRead | kMemoryProtectWrite); + + playback_event_ = xe::threading::Event::CreateAutoResetEvent(false); } TracePlayer::~TracePlayer() = default; @@ -77,6 +79,10 @@ void TracePlayer::SeekCommand(int target_command) { } } +void TracePlayer::WaitOnPlayback() { + xe::threading::Wait(playback_event_.get(), true); +} + void TracePlayer::PlayTrace(const uint8_t* trace_data, size_t trace_size, TracePlaybackMode playback_mode) { graphics_system_->command_processor()->CallInThread( @@ -194,6 +200,8 @@ void TracePlayer::PlayTraceOnThread(const uint8_t* trace_data, playing_trace_ = false; command_processor->set_swap_mode(SwapMode::kNormal); command_processor->IssueSwap(0, 1280, 720); + + playback_event_->Set(); } } // namespace gpu diff --git a/src/xenia/gpu/trace_player.h b/src/xenia/gpu/trace_player.h index 70678ec1c..dee6967ff 100644 --- a/src/xenia/gpu/trace_player.h +++ b/src/xenia/gpu/trace_player.h @@ -13,6 +13,7 @@ #include #include +#include "xenia/base/threading.h" #include "xenia/gpu/trace_protocol.h" #include "xenia/gpu/trace_reader.h" #include "xenia/ui/loop.h" @@ -35,7 +36,7 @@ class TracePlayer : public TraceReader { GraphicsSystem* graphics_system() const { return graphics_system_; } int current_frame_index() const { return current_frame_index_; } int current_command_index() const { return current_command_index_; } - bool playing_trace() const { return playing_trace_; } + bool is_playing_trace() const { return playing_trace_; } const Frame* current_frame() const; // Only valid if playing_trace is true. @@ -45,6 +46,8 @@ class TracePlayer : public TraceReader { void SeekFrame(int target_frame); void SeekCommand(int target_command); + void WaitOnPlayback(); + private: void PlayTrace(const uint8_t* trace_data, size_t trace_size, TracePlaybackMode playback_mode); @@ -57,6 +60,7 @@ class TracePlayer : public TraceReader { int current_command_index_; bool playing_trace_ = false; std::atomic playback_percent_ = 0; + std::unique_ptr playback_event_; }; } // namespace gpu diff --git a/src/xenia/gpu/trace_viewer.cc b/src/xenia/gpu/trace_viewer.cc index 54ce1fd64..f14516569 100644 --- a/src/xenia/gpu/trace_viewer.cc +++ b/src/xenia/gpu/trace_viewer.cc @@ -246,7 +246,7 @@ void TraceViewer::DrawControllerUI() { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Skip to last frame"); } - if (player_->playing_trace()) { + if (player_->is_playing_trace()) { // Don't allow the user to change the frame index just yet... // TODO: Find a way to disable the slider below. target_frame = player_->current_frame_index(); @@ -255,7 +255,7 @@ void TraceViewer::DrawControllerUI() { ImGui::SameLine(); ImGui::SliderInt("", &target_frame, 0, player_->frame_count() - 1); if (target_frame != player_->current_frame_index() && - !player_->playing_trace()) { + !player_->is_playing_trace()) { player_->SeekFrame(target_frame); } ImGui::End(); @@ -459,7 +459,7 @@ void TraceViewer::DrawCommandListUI() { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Move to the last command"); } - if (player_->playing_trace()) { + if (player_->is_playing_trace()) { // Don't allow the user to change the command index just yet... // TODO: Find a way to disable the slider below. target_command = player_->current_command_index(); @@ -470,7 +470,7 @@ void TraceViewer::DrawCommandListUI() { ImGui::PopItemWidth(); if (target_command != player_->current_command_index() && - !player_->playing_trace()) { + !player_->is_playing_trace()) { did_seek = true; player_->SeekCommand(target_command); } @@ -500,7 +500,7 @@ void TraceViewer::DrawCommandListUI() { break; } if (ImGui::Selectable(label, &is_selected)) { - if (!player_->playing_trace()) { + if (!player_->is_playing_trace()) { player_->SeekCommand(i); } } @@ -1003,7 +1003,7 @@ void TraceViewer::DrawStateUI() { } } - if (player_->playing_trace()) { + if (player_->is_playing_trace()) { ImGui::Text("Playing trace..."); float width = ImGui::GetWindowWidth() - 20.f; diff --git a/src/xenia/ui/gl/gl_context.cc b/src/xenia/ui/gl/gl_context.cc index 429e30387..7a6793bbf 100644 --- a/src/xenia/ui/gl/gl_context.cc +++ b/src/xenia/ui/gl/gl_context.cc @@ -466,6 +466,32 @@ void GLContext::EndSwap() { SwapBuffers(dc_); } +std::unique_ptr GLContext::Capture() { + GraphicsContextLock lock(this); + + std::unique_ptr raw_image(new RawImage()); + raw_image->width = target_window_->width(); + raw_image->stride = raw_image->width * 4; + raw_image->height = target_window_->height(); + raw_image->data.resize(raw_image->stride * raw_image->height); + + glReadPixels(0, 0, target_window_->width(), target_window_->height(), GL_RGBA, + GL_UNSIGNED_BYTE, raw_image->data.data()); + + // Flip vertically in-place. + size_t yt = 0; + size_t yb = (raw_image->height - 1) * raw_image->stride; + while (yt < yb) { + for (size_t i = 0; i < raw_image->stride; ++i) { + std::swap(raw_image->data[yt + i], raw_image->data[yb + i]); + } + yt += raw_image->stride; + yb -= raw_image->stride; + } + + return raw_image; +} + } // namespace gl } // namespace ui } // namespace xe diff --git a/src/xenia/ui/gl/gl_context.h b/src/xenia/ui/gl/gl_context.h index dd3389031..65d2beb5a 100644 --- a/src/xenia/ui/gl/gl_context.h +++ b/src/xenia/ui/gl/gl_context.h @@ -44,6 +44,8 @@ class GLContext : public GraphicsContext { void BeginSwap() override; void EndSwap() override; + std::unique_ptr Capture() override; + Blitter* blitter() { return &blitter_; } private: diff --git a/src/xenia/ui/graphics_context.h b/src/xenia/ui/graphics_context.h index 8c02a7b65..1ca00dbc8 100644 --- a/src/xenia/ui/graphics_context.h +++ b/src/xenia/ui/graphics_context.h @@ -11,6 +11,7 @@ #define XENIA_UI_GRAPHICS_CONTEXT_H_ #include +#include namespace xe { namespace ui { @@ -19,6 +20,17 @@ class GraphicsProvider; class ImmediateDrawer; class Window; +class RawImage { + public: + RawImage() = default; + ~RawImage() = default; + + size_t width = 0; + size_t height = 0; + size_t stride = 0; + std::vector data; +}; + class GraphicsContext { public: virtual ~GraphicsContext(); @@ -36,6 +48,8 @@ class GraphicsContext { virtual void BeginSwap() = 0; virtual void EndSwap() = 0; + virtual std::unique_ptr Capture() = 0; + protected: explicit GraphicsContext(GraphicsProvider* provider, Window* target_window);