#include "capture_panel.hpp" #include #include "imgui.h" #include "injection_panel.hpp" #include "ui/app_chrome.hpp" namespace coop { namespace { const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f); const ImVec4 kRed(1.0f, 0.45f, 0.4f, 1.0f); } // namespace bool CapturePanel::init(ID3D11Device* device) { device_ = device; shared_.init(device); // best effort; the Hooked source is unavailable if it fails return renderer_.init(device); } void CapturePanel::draw_ui(const FrameStats& stats) { apply_panel_layout(Panel::Video); ImGui::Begin("Video mirror"); const unsigned long hook_pid = injection_ != nullptr ? injection_->target_pid() : 0; const bool have_wgc_target = target_ != nullptr && IsWindow(target_); const bool have_hook = hook_pid != 0; const bool have_source = source_ == Source_Hooked ? have_hook : have_wgc_target; ImGui::BeginDisabled(!have_source); if (ImGui::Checkbox("Mirror game window", &enabled_) && !enabled_) { capture_.stop(); shared_.reset(); if (injection_ != nullptr) { injection_->request_video(false); // stop the in-game Present hook } } ImGui::EndDisabled(); // Source selector. Switching tears down the other source and (un)installs the // Present hook so the game only pays for the path actually in use. ImGui::TextUnformatted("Source:"); ImGui::SameLine(); int prev_source = source_; ImGui::RadioButton("WGC", &source_, Source_Wgc); ImGui::SameLine(); ImGui::RadioButton("Hooked (Present)", &source_, Source_Hooked); if (source_ != prev_source) { capture_.stop(); shared_.reset(); if (injection_ != nullptr) { injection_->request_video(enabled_ && source_ == Source_Hooked); } } if (!have_source) { ImGui::TextDisabled(source_ == Source_Hooked ? "Inject into a game first (the Present hook is the source)." : "Inject into a game first (its window is the source)."); } if (source_ == Source_Wgc) { // Start/restart WGC capture when enabled and the target window changes. if (enabled_ && have_wgc_target && capture_.target() != target_) { if (!capture_.start(target_, device_)) { enabled_ = false; ImGui::TextColored(kRed, "Failed to start capture."); } } if (capture_.running()) { ImGui::TextColored(kGreen, "Capturing %ux%u (WGC)", capture_.frame_width(), capture_.frame_height()); } } else // Source_Hooked { if (enabled_ && injection_ != nullptr) { // Keep the subsystem requested (a fresh inject may have reset control). if (!injection_->video_requested()) { injection_->request_video(true); } const VideoShareView share = injection_->video_share(); if (shared_.frames_copied() > 0 && shared_.width() > 0) { ImGui::TextColored(kGreen, "Mirroring %ux%u (hooked, %llu frames)", shared_.width(), shared_.height(), static_cast(shared_.frames_copied())); } else if (share.present_calls > 0) { ImGui::TextColored(kGreen, "Present hooked (%llu calls); opening shared texture...", static_cast(share.present_calls)); } else { ImGui::TextDisabled("Waiting for hooked frames (the game may not render via DXGI)."); } } } draw_perf_graphs(stats); ImGui::End(); } void CapturePanel::draw_perf_graphs(const FrameStats& stats) { // The mirror renders into this window, so the host's render frame timing is // the mirror's performance. Plot the recent history and summarise it. ImGui::SeparatorText("Render performance"); float ms[FrameStats::kHistory] = {}; const int n = stats.copy_frame_ms(ms); if (n == 0) { ImGui::TextDisabled("Gathering samples..."); return; } float fps[FrameStats::kHistory] = {}; for (int i = 0; i < n; ++i) { fps[i] = ms[i] > 0.0f ? 1000.0f / ms[i] : 0.0f; } float min_ms = 0.0f, max_ms = 0.0f, avg_ms = 0.0f; stats.history_stats(min_ms, max_ms, avg_ms); const float avg_fps = avg_ms > 0.0f ? 1000.0f / avg_ms : 0.0f; const float min_fps = max_ms > 0.0f ? 1000.0f / max_ms : 0.0f; // slowest frame -> lowest FPS const float max_fps = min_ms > 0.0f ? 1000.0f / min_ms : 0.0f; char overlay[64]; std::snprintf(overlay, sizeof(overlay), "avg %.2f ms", avg_ms); // Fixed 0..33 ms scale (30 FPS floor) so the line height is meaningful frame // to frame rather than auto-rescaling. ImGui::PlotLines("##frametime", ms, n, 0, overlay, 0.0f, 33.0f, ImVec2(-1.0f, 60.0f)); ImGui::Text("Frametime avg %.2f min %.2f max %.2f ms", avg_ms, min_ms, max_ms); std::snprintf(overlay, sizeof(overlay), "avg %.0f FPS", avg_fps); ImGui::PlotLines("##fps", fps, n, 0, overlay, 0.0f, 144.0f, ImVec2(-1.0f, 60.0f)); ImGui::Text("FPS avg %.0f min %.0f max %.0f", avg_fps, min_fps, max_fps); } void CapturePanel::render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h) { if (!enabled_) { return; } if (source_ == Source_Hooked) { if (injection_ == nullptr) { return; } if (shared_.update(injection_->video_share(), injection_->target_pid()) && shared_.srv() != nullptr) { renderer_.draw(ctx, shared_.srv(), shared_.width(), shared_.height(), dst_w, dst_h); } } else if (capture_.running()) { capture_.draw_latest(renderer_, ctx, dst_w, dst_h); } } } // namespace coop