Add F10 back-buffer screenshot (PNG, focus-independent)
Capture the rendered back buffer to a timestamped PNG next to the exe via WIC, triggered by F10 (delivered even when unfocused). The capture runs in render_frame just before Present so it includes the ImGui overlay, and reads off the GPU so it works regardless of window focus, z-order, or occlusion. A brief toast confirms the save (drawn the next frame, so it's never in the shot). F10 chosen to avoid Steam's F12; its WM_SYSKEYDOWN is swallowed so Windows doesn't enter menu mode. Documented in the Help menu + README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
// this window via Windows Graphics Capture (Video mirror panel).
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
@@ -18,6 +20,7 @@
|
||||
#include "audio_panel.hpp"
|
||||
#include "capture_panel.hpp"
|
||||
#include "controllers_panel.hpp"
|
||||
#include "coop/tool_paths.hpp"
|
||||
#include "d3d11_window.hpp"
|
||||
#include "imgui_layer.hpp"
|
||||
#include "inject/mkb_forward.hpp"
|
||||
@@ -26,13 +29,59 @@
|
||||
#include "log_panel.hpp"
|
||||
#include "ui/app_chrome.hpp"
|
||||
|
||||
#ifdef COOP_WITH_STEAM
|
||||
#include <string>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Timestamped screenshot path next to the exe (e.g. coop_shot_20260622_143501.png).
|
||||
std::wstring screenshot_path()
|
||||
{
|
||||
SYSTEMTIME st{};
|
||||
GetLocalTime(&st);
|
||||
wchar_t name[64];
|
||||
swprintf(name, static_cast<int>(std::size(name)), L"coop_shot_%04u%02u%02u_%02u%02u%02u.png", st.wYear,
|
||||
st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
|
||||
return coop::exe_directory() + name;
|
||||
}
|
||||
|
||||
// Just the filename of a path, narrowed to UTF-8 for an ImGui confirmation toast.
|
||||
std::string screenshot_basename(const std::wstring& path)
|
||||
{
|
||||
const std::size_t slash = path.find_last_of(L"\\/");
|
||||
const std::wstring file = slash == std::wstring::npos ? path : path.substr(slash + 1);
|
||||
if (file.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
const int n = WideCharToMultiByte(CP_UTF8, 0, file.c_str(), static_cast<int>(file.size()), nullptr, 0,
|
||||
nullptr, nullptr);
|
||||
std::string out(static_cast<std::size_t>(n), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, file.c_str(), static_cast<int>(file.size()), out.data(), n, nullptr, nullptr);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Brief fading "saved" confirmation after an F10 screenshot (bottom-left, non-interactive
|
||||
// so it never steals a click). It's drawn the frame *after* the capture, so it never lands
|
||||
// in the shot itself.
|
||||
void draw_screenshot_toast(double seconds_since, const std::string& name)
|
||||
{
|
||||
const float fade = 1.0f - static_cast<float>(seconds_since) / 2.5f;
|
||||
if (fade <= 0.0f || name.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(ImVec2(vp->WorkPos.x + 12.0f, vp->WorkPos.y + vp->WorkSize.y - 44.0f));
|
||||
ImGui::SetNextWindowBgAlpha(0.45f * fade);
|
||||
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs |
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
|
||||
ImGui::Begin("##shot_toast", nullptr, flags);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 1.0f, 0.6f, fade));
|
||||
ImGui::Text("Saved screenshot: %s", name.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
#ifdef COOP_WITH_STEAM
|
||||
// Absolute path to the bundled Steam Input action manifest (next to the exe).
|
||||
std::string steam_manifest_path()
|
||||
@@ -151,6 +200,8 @@ int run()
|
||||
// Play Together; the pipelines keep running underneath either way.
|
||||
bool show_overlay = true;
|
||||
double overlay_hidden_at = 0.0;
|
||||
double last_shot_at = -10.0; // when the last F10 screenshot was saved (for the toast)
|
||||
std::string last_shot_name;
|
||||
coop::UiState ui;
|
||||
// Persist the "Debug details" verbosity in the .ini. Register before the first
|
||||
// begin_frame() below, which is when ImGui loads the .ini and replays our handler.
|
||||
@@ -210,6 +261,10 @@ int run()
|
||||
{
|
||||
injection.toggle_cursor_release(); // free/clip the operator's mouse for clipping games
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F10, false))
|
||||
{
|
||||
window.request_screenshot(screenshot_path()); // captured at Present, overlay included
|
||||
}
|
||||
|
||||
if (show_overlay)
|
||||
{
|
||||
@@ -234,6 +289,7 @@ int run()
|
||||
{
|
||||
log.draw();
|
||||
}
|
||||
draw_screenshot_toast(ImGui::GetTime() - last_shot_at, last_shot_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -265,6 +321,14 @@ int run()
|
||||
imgui.end_frame();
|
||||
},
|
||||
sync_interval);
|
||||
|
||||
// render_frame saves a pending F10 screenshot just before Present; pick up the
|
||||
// result here so next frame shows the confirmation toast (kept out of the shot).
|
||||
if (std::wstring shot = window.take_screenshot_result(); !shot.empty())
|
||||
{
|
||||
last_shot_at = ImGui::GetTime();
|
||||
last_shot_name = screenshot_basename(shot);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user