From 2dc5b213418f348deb29125f386094971262c78e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 18 Mar 2026 10:47:34 -0700 Subject: [PATCH] feat: add screenshot capture (PrintScreen key and /screenshot command) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures the Vulkan swapchain image to PNG via stb_image_write. Screenshots saved to ~/.wowee/screenshots/ with timestamped filenames. Cross-platform: BGRA→RGBA swizzle, localtime_r/localtime_s. --- include/rendering/renderer.hpp | 3 ++ include/ui/game_screen.hpp | 1 + src/rendering/renderer.cpp | 99 ++++++++++++++++++++++++++++++++++ src/ui/game_screen.cpp | 50 ++++++++++++++++- 4 files changed, 152 insertions(+), 1 deletion(-) diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 1f33d2f4..588fa3af 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -154,6 +154,9 @@ public: void triggerLevelUpEffect(const glm::vec3& position); void cancelEmote(); + // Screenshot capture — copies swapchain image to PNG file + bool captureScreenshot(const std::string& outputPath); + // Spell visual effects (SMSG_PLAY_SPELL_VISUAL / SMSG_PLAY_SPELL_IMPACT) // useImpactKit=false → CastKit path; useImpactKit=true → ImpactKit path void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition, diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 0054bf05..95e07994 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -398,6 +398,7 @@ private: void renderBattlegroundScore(game::GameHandler& gameHandler); void renderDPSMeter(game::GameHandler& gameHandler); void renderDurabilityWarning(game::GameHandler& gameHandler); + void takeScreenshot(game::GameHandler& gameHandler); /** * Inventory screen diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 4da8bad7..2d942c23 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -67,6 +67,10 @@ #include #include #include +#include + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "stb_image_write.h" #include #include #include @@ -2574,6 +2578,101 @@ void Renderer::cancelEmote() { emoteLoop = false; } +bool Renderer::captureScreenshot(const std::string& outputPath) { + if (!vkCtx) return false; + + VkDevice device = vkCtx->getDevice(); + VmaAllocator alloc = vkCtx->getAllocator(); + VkExtent2D extent = vkCtx->getSwapchainExtent(); + const auto& images = vkCtx->getSwapchainImages(); + + if (images.empty() || currentImageIndex >= images.size()) return false; + + VkImage srcImage = images[currentImageIndex]; + uint32_t w = extent.width; + uint32_t h = extent.height; + VkDeviceSize bufSize = static_cast(w) * h * 4; + + // Stall GPU so the swapchain image is idle + vkDeviceWaitIdle(device); + + // Create staging buffer + VkBufferCreateInfo bufInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bufInfo.size = bufSize; + bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + + VmaAllocationCreateInfo allocCI{}; + allocCI.usage = VMA_MEMORY_USAGE_CPU_ONLY; + + VkBuffer stagingBuf = VK_NULL_HANDLE; + VmaAllocation stagingAlloc = VK_NULL_HANDLE; + if (vmaCreateBuffer(alloc, &bufInfo, &allocCI, &stagingBuf, &stagingAlloc, nullptr) != VK_SUCCESS) { + LOG_WARNING("Screenshot: failed to create staging buffer"); + return false; + } + + // Record copy commands + VkCommandBuffer cmd = vkCtx->beginSingleTimeCommands(); + + // Transition swapchain image: PRESENT_SRC → TRANSFER_SRC + VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransfer.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT; + toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toTransfer.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toTransfer.image = srcImage; + toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &toTransfer); + + // Copy image to buffer + VkBufferImageCopy region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {w, h, 1}; + vkCmdCopyImageToBuffer(cmd, srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + stagingBuf, 1, ®ion); + + // Transition back: TRANSFER_SRC → PRESENT_SRC + VkImageMemoryBarrier toPresent = toTransfer; + toPresent.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toPresent.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT; + toPresent.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toPresent.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + 0, 0, nullptr, 0, nullptr, 1, &toPresent); + + vkCtx->endSingleTimeCommands(cmd); + + // Map and convert BGRA → RGBA + void* mapped = nullptr; + vmaMapMemory(alloc, stagingAlloc, &mapped); + auto* pixels = static_cast(mapped); + for (uint32_t i = 0; i < w * h; ++i) { + std::swap(pixels[i * 4 + 0], pixels[i * 4 + 2]); // B ↔ R + } + + // Ensure output directory exists + std::filesystem::path outPath(outputPath); + if (outPath.has_parent_path()) + std::filesystem::create_directories(outPath.parent_path()); + + int ok = stbi_write_png(outputPath.c_str(), + static_cast(w), static_cast(h), + 4, pixels, static_cast(w * 4)); + + vmaUnmapMemory(alloc, stagingAlloc); + vmaDestroyBuffer(alloc, stagingBuf, stagingAlloc); + + if (ok) { + LOG_INFO("Screenshot saved: ", outputPath); + } else { + LOG_WARNING("Screenshot: stbi_write_png failed for ", outputPath); + } + return ok != 0; +} + void Renderer::triggerLevelUpEffect(const glm::vec3& position) { if (!levelUpEffect) return; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 0d680919..a216723d 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -2809,6 +2809,11 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { showTitlesWindow_ = !showTitlesWindow_; } + // Screenshot (PrintScreen key) + if (input.isKeyJustPressed(SDL_SCANCODE_PRINTSCREEN)) { + takeScreenshot(gameHandler); + } + // Action bar keys (1-9, 0, -, =) static const SDL_Scancode actionBarKeys[] = { SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4, @@ -5899,6 +5904,13 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /screenshot command — capture current frame to PNG + if (cmdLower == "screenshot" || cmdLower == "ss") { + takeScreenshot(gameHandler); + chatInputBuffer[0] = '\0'; + return; + } + // /zone command — print current zone name if (cmdLower == "zone") { std::string zoneName; @@ -5978,7 +5990,7 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { "Movement: /sit /stand /kneel /dismount", "Misc: /played /time /zone /loc /afk [msg] /dnd [msg] /inspect", " /helm /cloak /trade /join /leave ", - " /score /unstuck /logout /ticket /help", + " /score /unstuck /logout /ticket /screenshot /help", }; for (const char* line : kHelpLines) { game::MessageChatData helpMsg; @@ -12160,6 +12172,42 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { // Durability Warning (equipment damage indicator) // ============================================================ +void GameScreen::takeScreenshot(game::GameHandler& /*gameHandler*/) { + auto* renderer = core::Application::getInstance().getRenderer(); + if (!renderer) return; + + // Build path: ~/.wowee/screenshots/WoWee_YYYYMMDD_HHMMSS.png + const char* home = std::getenv("HOME"); + if (!home) home = std::getenv("USERPROFILE"); + if (!home) home = "/tmp"; + std::string dir = std::string(home) + "/.wowee/screenshots"; + + auto now = std::chrono::system_clock::now(); + auto tt = std::chrono::system_clock::to_time_t(now); + std::tm tm{}; +#ifdef _WIN32 + localtime_s(&tm, &tt); +#else + localtime_r(&tt, &tm); +#endif + + char filename[128]; + std::snprintf(filename, sizeof(filename), + "WoWee_%04d%02d%02d_%02d%02d%02d.png", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec); + + std::string path = dir + "/" + filename; + + if (renderer->captureScreenshot(path)) { + game::MessageChatData sysMsg; + sysMsg.type = game::ChatType::SYSTEM; + sysMsg.language = game::ChatLanguage::UNIVERSAL; + sysMsg.message = "Screenshot saved: " + path; + core::Application::getInstance().getGameHandler()->addLocalChatMessage(sysMsg); + } +} + void GameScreen::renderDurabilityWarning(game::GameHandler& gameHandler) { if (gameHandler.getPlayerGuid() == 0) return;