mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-04-29 23:41:19 -06:00
Implement screenshot functionality with overlays and game-only options (#4248)
* Implement screenshot functionality with overlays and game-only options
* CLang 🤦
* video: F12 game screenshot, Alt+F12 HUD screenshot
- Capture game-only screenshots from the VideoOut image before FSR/PP scaling (native guest output res)
- Capture overlay screenshots from the swapchain/output image; HDR screenshots are tone-mapped to SDR PNG
- Split screenshot request counters + consumption for game-only vs with-overlays
- Add A2R10G10B10 readback handling and force opaque alpha in PNG output
- Update default hotkeys (keep backward-compat with hotkey_renderdoc_capture)
- Ignore tmp/artifacts/
* Add legacy capture binding support in input configuration
---------
Co-authored-by: w1naenator <valdis.bogdans@hotmail.com>
This commit is contained in:
parent
cfa5838a13
commit
311c2dd1cd
@ -108,7 +108,8 @@ For more information on how to test, debug and report issues with the emulator o
|
||||
F10 | FPS Counter
|
||||
Ctrl+F10 | Video Debug Info
|
||||
F11 | Fullscreen
|
||||
F12 | Trigger RenderDoc Capture
|
||||
F12 | Trigger RenderDoc Capture (or game-only screenshot if RenderDoc is unavailable)
|
||||
Alt+F12 | Capture screenshot including HUD/dialog overlays
|
||||
|
||||
> [!NOTE]
|
||||
> Xbox and DualShock controllers work out of the box.
|
||||
|
||||
@ -1436,7 +1436,8 @@ std::filesystem::path GetInputConfigFile(const string& game_id) {
|
||||
}
|
||||
if (game_id == "global") {
|
||||
std::map<string, string> default_bindings_to_add = {
|
||||
{"hotkey_renderdoc_capture", "f12"},
|
||||
{"hotkey_capture_frame", "f12"},
|
||||
{"hotkey_screenshot_with_overlays", "lalt, f12"},
|
||||
{"hotkey_fullscreen", "f11"},
|
||||
{"hotkey_show_fps", "f10"},
|
||||
{"hotkey_pause", "f9"},
|
||||
@ -1448,6 +1449,8 @@ std::filesystem::path GetInputConfigFile(const string& game_id) {
|
||||
{"hotkey_volume_up", "kpplus"},
|
||||
{"hotkey_volume_down", "kpminus"},
|
||||
};
|
||||
string legacy_capture_binding;
|
||||
bool legacy_capture_binding_found = false;
|
||||
std::ifstream global_in(config_file);
|
||||
string line;
|
||||
while (std::getline(global_in, line)) {
|
||||
@ -1459,9 +1462,19 @@ std::filesystem::path GetInputConfigFile(const string& game_id) {
|
||||
continue;
|
||||
}
|
||||
std::string output_string = line.substr(0, equal_pos);
|
||||
if (output_string == "hotkey_renderdoc_capture") {
|
||||
legacy_capture_binding = line.substr(equal_pos + 1);
|
||||
legacy_capture_binding_found = true;
|
||||
}
|
||||
default_bindings_to_add.erase(output_string);
|
||||
}
|
||||
global_in.close();
|
||||
if (legacy_capture_binding_found) {
|
||||
if (auto it = default_bindings_to_add.find("hotkey_capture_frame");
|
||||
it != default_bindings_to_add.end()) {
|
||||
it->second = legacy_capture_binding;
|
||||
}
|
||||
}
|
||||
std::ofstream global_out(config_file, std::ios::app);
|
||||
for (auto const& b : default_bindings_to_add) {
|
||||
global_out << b.first << " = " << b.second << "\n";
|
||||
|
||||
@ -166,7 +166,8 @@ std::filesystem::path GetInputConfigFile(const std::string& game_id) {
|
||||
}
|
||||
if (game_id == "global") {
|
||||
std::map<std::string, std::string> default_bindings_to_add = {
|
||||
{"hotkey_renderdoc_capture", "f12"},
|
||||
{"hotkey_capture_frame", "f12"},
|
||||
{"hotkey_screenshot_with_overlays", "lalt, f12"},
|
||||
{"hotkey_fullscreen", "f11"},
|
||||
{"hotkey_show_fps", "f10"},
|
||||
{"hotkey_pause", "f9"},
|
||||
@ -180,6 +181,8 @@ std::filesystem::path GetInputConfigFile(const std::string& game_id) {
|
||||
{"hotkey_volume_up", "kpplus"},
|
||||
{"hotkey_volume_down", "kpminus"},
|
||||
};
|
||||
std::string legacy_capture_binding;
|
||||
bool legacy_capture_binding_found = false;
|
||||
std::ifstream global_in(config_file);
|
||||
std::string line;
|
||||
while (std::getline(global_in, line)) {
|
||||
@ -191,9 +194,19 @@ std::filesystem::path GetInputConfigFile(const std::string& game_id) {
|
||||
continue;
|
||||
}
|
||||
std::string output_string = line.substr(0, equal_pos);
|
||||
if (output_string == "hotkey_renderdoc_capture") {
|
||||
legacy_capture_binding = line.substr(equal_pos + 1);
|
||||
legacy_capture_binding_found = true;
|
||||
}
|
||||
default_bindings_to_add.erase(output_string);
|
||||
}
|
||||
global_in.close();
|
||||
if (legacy_capture_binding_found) {
|
||||
if (auto it = default_bindings_to_add.find("hotkey_capture_frame");
|
||||
it != default_bindings_to_add.end()) {
|
||||
it->second = legacy_capture_binding;
|
||||
}
|
||||
}
|
||||
std::ofstream global_out(config_file, std::ios::app);
|
||||
for (auto const& b : default_bindings_to_add) {
|
||||
global_out << b.first << " = " << b.second << "\n";
|
||||
@ -699,6 +712,7 @@ void ControllerOutput::AddUpdate(InputEvent event) {
|
||||
*new_param = (event.active ? event.axis_value : 0) + *new_param;
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerOutput::FinalizeUpdate(u8 gamepad_index) {
|
||||
auto PushSDLEvent = [&](u32 event_type) {
|
||||
if (new_button_state) {
|
||||
@ -763,6 +777,9 @@ void ControllerOutput::FinalizeUpdate(u8 gamepad_index) {
|
||||
case HOTKEY_RENDERDOC:
|
||||
PushSDLEvent(SDL_EVENT_RDOC_CAPTURE);
|
||||
break;
|
||||
case HOTKEY_SCREENSHOT_WITH_OVERLAYS:
|
||||
PushSDLEvent(SDL_EVENT_SCREENSHOT_WITH_OVERLAYS);
|
||||
break;
|
||||
case HOTKEY_ADD_VIRTUAL_USER:
|
||||
PushSDLEvent(SDL_EVENT_ADD_VIRTUAL_USER);
|
||||
break;
|
||||
|
||||
@ -41,6 +41,7 @@
|
||||
#define SDL_EVENT_ADD_VIRTUAL_USER SDL_EVENT_USER + 11
|
||||
#define SDL_EVENT_REMOVE_VIRTUAL_USER SDL_EVENT_USER + 12
|
||||
#define SDL_EVENT_RDOC_CAPTURE SDL_EVENT_USER + 13
|
||||
#define SDL_EVENT_SCREENSHOT_WITH_OVERLAYS SDL_EVENT_USER + 14
|
||||
|
||||
#define LEFTJOYSTICK_HALFMODE 0x00010000
|
||||
#define RIGHTJOYSTICK_HALFMODE 0x00020000
|
||||
@ -62,6 +63,7 @@
|
||||
#define HOTKEY_VOLUME_DOWN 0xf000000b
|
||||
#define HOTKEY_ADD_VIRTUAL_USER 0xf000000c
|
||||
#define HOTKEY_REMOVE_VIRTUAL_USER 0xf000000d
|
||||
#define HOTKEY_SCREENSHOT_WITH_OVERLAYS 0xf000000e
|
||||
|
||||
#define SDL_UNMAPPED UINT32_MAX - 1
|
||||
|
||||
@ -156,6 +158,8 @@ const std::map<std::string, u32> string_to_hotkey_map = {
|
||||
{"hotkey_toggle_mouse_to_joystick", HOTKEY_TOGGLE_MOUSE_TO_JOYSTICK},
|
||||
{"hotkey_toggle_mouse_to_gyro", HOTKEY_TOGGLE_MOUSE_TO_GYRO},
|
||||
{"hotkey_toggle_mouse_to_touchpad", HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD},
|
||||
{"hotkey_capture_frame", HOTKEY_RENDERDOC},
|
||||
{"hotkey_screenshot_with_overlays", HOTKEY_SCREENSHOT_WITH_OVERLAYS},
|
||||
{"hotkey_renderdoc_capture", HOTKEY_RENDERDOC},
|
||||
{"hotkey_add_virtual_user", HOTKEY_ADD_VIRTUAL_USER},
|
||||
{"hotkey_remove_virtual_user", HOTKEY_REMOVE_VIRTUAL_USER},
|
||||
@ -526,7 +530,7 @@ public:
|
||||
|
||||
class ControllerAllOutputs {
|
||||
public:
|
||||
static constexpr u64 output_count = 40;
|
||||
static constexpr u64 output_count = 41;
|
||||
std::array<ControllerOutput, output_count> data = {
|
||||
// Important: these have to be the first, or else they will update in the wrong order
|
||||
ControllerOutput(LEFTJOYSTICK_HALFMODE),
|
||||
@ -574,6 +578,7 @@ public:
|
||||
ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_GYRO),
|
||||
ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD),
|
||||
ControllerOutput(HOTKEY_RENDERDOC),
|
||||
ControllerOutput(HOTKEY_SCREENSHOT_WITH_OVERLAYS),
|
||||
ControllerOutput(HOTKEY_ADD_VIRTUAL_USER),
|
||||
ControllerOutput(HOTKEY_REMOVE_VIRTUAL_USER),
|
||||
ControllerOutput(HOTKEY_VOLUME_UP),
|
||||
|
||||
@ -285,7 +285,14 @@ void WindowSDL::WaitEvent() {
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_RDOC_CAPTURE:
|
||||
VideoCore::TriggerCapture();
|
||||
if (VideoCore::IsRenderDocLoaded()) {
|
||||
VideoCore::TriggerCapture();
|
||||
} else {
|
||||
VideoCore::RequestScreenshot(VideoCore::ScreenshotRequest::GameOnly);
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_SCREENSHOT_WITH_OVERLAYS:
|
||||
VideoCore::RequestScreenshot(VideoCore::ScreenshotRequest::WithOverlays);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include "core/emulator_settings.h"
|
||||
#include "video_core/renderdoc.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <renderdoc_app.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
@ -23,6 +24,8 @@ enum class CaptureState {
|
||||
InProgress,
|
||||
};
|
||||
static CaptureState capture_state{CaptureState::Idle};
|
||||
static std::atomic<u32> screenshot_game_only_count{0};
|
||||
static std::atomic<u32> screenshot_with_overlays_count{0};
|
||||
|
||||
RENDERDOC_API_1_6_0* rdoc_api{};
|
||||
|
||||
@ -125,4 +128,37 @@ void SetOutputDir(const std::filesystem::path& path, const std::string& prefix)
|
||||
rdoc_api->SetCaptureFilePathTemplate(fmt::UTF((path / prefix).u8string()).data.data());
|
||||
}
|
||||
|
||||
bool IsRenderDocLoaded() {
|
||||
return rdoc_api != nullptr;
|
||||
}
|
||||
|
||||
void RequestScreenshot(const ScreenshotRequest request) {
|
||||
switch (request) {
|
||||
case ScreenshotRequest::GameOnly:
|
||||
screenshot_game_only_count.fetch_add(1, std::memory_order_relaxed);
|
||||
break;
|
||||
case ScreenshotRequest::WithOverlays:
|
||||
screenshot_with_overlays_count.fetch_add(1, std::memory_order_relaxed);
|
||||
break;
|
||||
case ScreenshotRequest::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
u32 ConsumeGameOnlyScreenshotRequests() {
|
||||
return screenshot_game_only_count.exchange(0, std::memory_order_acq_rel);
|
||||
}
|
||||
|
||||
u32 ConsumeWithOverlaysScreenshotRequests() {
|
||||
return screenshot_with_overlays_count.exchange(0, std::memory_order_acq_rel);
|
||||
}
|
||||
|
||||
ScreenshotRequests ConsumeScreenshotRequests() {
|
||||
return ScreenshotRequests{
|
||||
.game_only_count = ConsumeGameOnlyScreenshotRequests(),
|
||||
.with_overlays_count = ConsumeWithOverlaysScreenshotRequests(),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace VideoCore
|
||||
|
||||
@ -3,7 +3,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include "common/types.h"
|
||||
|
||||
namespace VideoCore {
|
||||
|
||||
@ -22,4 +24,30 @@ void TriggerCapture();
|
||||
/// Sets output directory for captures
|
||||
void SetOutputDir(const std::filesystem::path& path, const std::string& prefix);
|
||||
|
||||
/// Returns true when RenderDoc API was loaded and is usable.
|
||||
bool IsRenderDocLoaded();
|
||||
|
||||
enum class ScreenshotRequest : u32 {
|
||||
None = 0,
|
||||
GameOnly = 1,
|
||||
WithOverlays = 2,
|
||||
};
|
||||
|
||||
struct ScreenshotRequests {
|
||||
u32 game_only_count{};
|
||||
u32 with_overlays_count{};
|
||||
};
|
||||
|
||||
/// Queues an in-emulator screenshot request to be consumed by the presenter.
|
||||
void RequestScreenshot(ScreenshotRequest request);
|
||||
|
||||
/// Atomically consumes and returns pending "game only" screenshot request counter.
|
||||
u32 ConsumeGameOnlyScreenshotRequests();
|
||||
|
||||
/// Atomically consumes and returns pending "with overlays" screenshot request counter.
|
||||
u32 ConsumeWithOverlaysScreenshotRequests();
|
||||
|
||||
/// Atomically consumes and returns pending screenshot request counters.
|
||||
ScreenshotRequests ConsumeScreenshotRequests();
|
||||
|
||||
} // namespace VideoCore
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/elf_info.h"
|
||||
#include "common/io_file.h"
|
||||
#include "common/path_util.h"
|
||||
#include "common/singleton.h"
|
||||
#include "core/debug_state.h"
|
||||
#include "core/devtools/layer.h"
|
||||
@ -11,12 +13,32 @@
|
||||
#include "imgui/renderer/imgui_core.h"
|
||||
#include "imgui/renderer/imgui_impl_vulkan.h"
|
||||
#include "sdl_window.h"
|
||||
#include "video_core/buffer_cache/buffer.h"
|
||||
#include "video_core/renderdoc.h"
|
||||
#include "video_core/renderer_vulkan/vk_platform.h"
|
||||
#include "video_core/renderer_vulkan/vk_presenter.h"
|
||||
#include "video_core/renderer_vulkan/vk_rasterizer.h"
|
||||
#include "video_core/texture_cache/image.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <csetjmp>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <sstream>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
#include <imgui.h>
|
||||
#include <png.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
namespace Vulkan {
|
||||
@ -102,6 +124,353 @@ static vk::Rect2D FitImage(s32 frame_width, s32 frame_height, s32 swapchain_widt
|
||||
dst_rect.offset.x, dst_rect.offset.y);
|
||||
}
|
||||
|
||||
enum class ScreenshotKind : u8 {
|
||||
GameOnly,
|
||||
WithOverlays,
|
||||
};
|
||||
|
||||
struct ScreenshotReadback {
|
||||
ScreenshotKind kind{};
|
||||
std::vector<std::filesystem::path> paths{};
|
||||
VideoCore::Buffer buffer;
|
||||
u32 width{};
|
||||
u32 height{};
|
||||
vk::Format format{};
|
||||
bool hdr_encoded{};
|
||||
|
||||
ScreenshotReadback(const Instance& instance, Scheduler& scheduler, ScreenshotKind kind_,
|
||||
std::vector<std::filesystem::path> paths_, const u32 width_,
|
||||
const u32 height_, const vk::Format format_, const bool hdr_encoded_)
|
||||
: kind{kind_}, paths{std::move(paths_)},
|
||||
buffer{instance,
|
||||
scheduler,
|
||||
VideoCore::MemoryUsage::Download,
|
||||
0,
|
||||
vk::BufferUsageFlagBits::eTransferDst,
|
||||
static_cast<u64>(width_) * static_cast<u64>(height_) * 4},
|
||||
width{width_}, height{height_}, format{format_}, hdr_encoded{hdr_encoded_} {}
|
||||
};
|
||||
|
||||
static std::string SanitizeFilenameComponent(std::string value) {
|
||||
for (char& c : value) {
|
||||
const unsigned char uc = static_cast<unsigned char>(c);
|
||||
if (!std::isalnum(uc) && c != '_' && c != '-') {
|
||||
c = '_';
|
||||
}
|
||||
}
|
||||
if (value.empty()) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static std::vector<std::filesystem::path> BuildScreenshotPaths(const ScreenshotKind kind,
|
||||
const u32 count) {
|
||||
static std::atomic<u64> screenshot_sequence{0};
|
||||
std::vector<std::filesystem::path> paths{};
|
||||
if (count == 0) {
|
||||
return paths;
|
||||
}
|
||||
|
||||
const auto& screenshots_dir = Common::FS::GetUserPath(Common::FS::PathType::ScreenshotsDir);
|
||||
std::filesystem::create_directories(screenshots_dir);
|
||||
|
||||
const auto game_id =
|
||||
SanitizeFilenameComponent(std::string(Common::ElfInfo::Instance().GameSerial()));
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const auto now_time = std::chrono::system_clock::to_time_t(now);
|
||||
const auto ms =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count() %
|
||||
1000;
|
||||
|
||||
std::tm local_tm{};
|
||||
#ifdef _WIN32
|
||||
localtime_s(&local_tm, &now_time);
|
||||
#else
|
||||
localtime_r(&now_time, &local_tm);
|
||||
#endif
|
||||
|
||||
std::ostringstream stamp;
|
||||
stamp << std::put_time(&local_tm, "%Y%m%d_%H%M%S") << '_' << std::setw(3) << std::setfill('0')
|
||||
<< ms;
|
||||
|
||||
const char* suffix = kind == ScreenshotKind::GameOnly ? "game" : "hud";
|
||||
const auto first_sequence = screenshot_sequence.fetch_add(count, std::memory_order_relaxed);
|
||||
|
||||
paths.reserve(count);
|
||||
const auto stamp_str = stamp.str();
|
||||
for (u32 i = 0; i < count; ++i) {
|
||||
paths.emplace_back(screenshots_dir / fmt::format("{}_{}_{}_{:06}.png", game_id, stamp_str,
|
||||
suffix, first_sequence + i));
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
static float PqToNits(const float encoded) {
|
||||
// ST.2084 inverse EOTF
|
||||
constexpr float m1 = 2610.0f / 16384.0f;
|
||||
constexpr float m2 = 2523.0f / 32.0f;
|
||||
constexpr float c1 = 3424.0f / 4096.0f;
|
||||
constexpr float c2 = 2413.0f / 128.0f;
|
||||
constexpr float c3 = 2392.0f / 128.0f;
|
||||
|
||||
const float v = std::clamp(encoded, 0.0f, 1.0f);
|
||||
const float vp = std::pow(v, 1.0f / m2);
|
||||
const float num = std::max(vp - c1, 0.0f);
|
||||
const float den = std::max(c2 - c3 * vp, 1e-6f);
|
||||
return 10000.0f * std::pow(num / den, 1.0f / m1);
|
||||
}
|
||||
|
||||
static float ToneMapToSdrLinear(const float nits) {
|
||||
// Map absolute HDR luminance into SDR [0,1], preserving 100-nit white.
|
||||
constexpr float sdr_white_nits = 100.0f;
|
||||
const float x = std::max(nits, 0.0f) / sdr_white_nits;
|
||||
const float mapped = (2.0f * x) / (1.0f + x);
|
||||
return std::clamp(mapped, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
static float LinearToSrgb(const float linear) {
|
||||
const float x = std::clamp(linear, 0.0f, 1.0f);
|
||||
if (x <= 0.0031308f) {
|
||||
return 12.92f * x;
|
||||
}
|
||||
return 1.055f * std::pow(x, 1.0f / 2.4f) - 0.055f;
|
||||
}
|
||||
|
||||
static const std::array<float, 1024>& GetPqDecodeNitsLut() {
|
||||
static const std::array<float, 1024> lut = [] {
|
||||
std::array<float, 1024> values{};
|
||||
for (size_t i = 0; i < values.size(); ++i) {
|
||||
values[i] = PqToNits(static_cast<float>(i) / 1023.0f);
|
||||
}
|
||||
return values;
|
||||
}();
|
||||
return lut;
|
||||
}
|
||||
|
||||
static const std::array<u8, 1024>& GetUnorm10ToU8Lut() {
|
||||
static const std::array<u8, 1024> lut = [] {
|
||||
std::array<u8, 1024> values{};
|
||||
for (size_t i = 0; i < values.size(); ++i) {
|
||||
values[i] = static_cast<u8>((i * 255u + 511u) / 1023u);
|
||||
}
|
||||
return values;
|
||||
}();
|
||||
return lut;
|
||||
}
|
||||
|
||||
static void CopyImageToReadback(const vk::CommandBuffer& cmdbuf, const vk::Image image,
|
||||
const vk::ImageLayout layout, ScreenshotReadback& readback) {
|
||||
const vk::BufferImageCopy copy_region = {
|
||||
.bufferOffset = 0,
|
||||
.bufferRowLength = 0,
|
||||
.bufferImageHeight = 0,
|
||||
.imageSubresource =
|
||||
{
|
||||
.aspectMask = vk::ImageAspectFlagBits::eColor,
|
||||
.mipLevel = 0,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = 1,
|
||||
},
|
||||
.imageOffset = {0, 0, 0},
|
||||
.imageExtent = {readback.width, readback.height, 1},
|
||||
};
|
||||
cmdbuf.copyImageToBuffer(image, layout, readback.buffer.Handle(), copy_region);
|
||||
}
|
||||
|
||||
static bool ConvertReadbackToRgba8(const ScreenshotReadback& readback, std::vector<u8>& out_rgba) {
|
||||
const u64 pixel_count = static_cast<u64>(readback.width) * static_cast<u64>(readback.height);
|
||||
const u64 byte_size = pixel_count * 4;
|
||||
if (readback.buffer.mapped_data.size() < byte_size) {
|
||||
LOG_ERROR(Render_Vulkan, "Screenshot readback buffer size mismatch (have {}, need {})",
|
||||
readback.buffer.mapped_data.size(), byte_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto src =
|
||||
std::span<const u8>{readback.buffer.mapped_data.data(), static_cast<size_t>(byte_size)};
|
||||
out_rgba.resize(static_cast<size_t>(byte_size));
|
||||
|
||||
switch (readback.format) {
|
||||
case vk::Format::eR8G8B8A8Unorm:
|
||||
case vk::Format::eR8G8B8A8Srgb:
|
||||
std::memcpy(out_rgba.data(), src.data(), out_rgba.size());
|
||||
for (u64 i = 0; i < pixel_count; ++i) {
|
||||
out_rgba[static_cast<size_t>(i) * 4 + 3] = 255;
|
||||
}
|
||||
return true;
|
||||
case vk::Format::eB8G8R8A8Unorm:
|
||||
case vk::Format::eB8G8R8A8Srgb:
|
||||
for (u64 i = 0; i < pixel_count; ++i) {
|
||||
const size_t o = static_cast<size_t>(i) * 4;
|
||||
out_rgba[o + 0] = src[o + 2];
|
||||
out_rgba[o + 1] = src[o + 1];
|
||||
out_rgba[o + 2] = src[o + 0];
|
||||
out_rgba[o + 3] = 255;
|
||||
}
|
||||
return true;
|
||||
case vk::Format::eA2R10G10B10UnormPack32: {
|
||||
const auto& pq_decode_lut = GetPqDecodeNitsLut();
|
||||
const auto& unorm10_to_u8 = GetUnorm10ToU8Lut();
|
||||
|
||||
for (u64 i = 0; i < pixel_count; ++i) {
|
||||
const size_t o = static_cast<size_t>(i) * 4;
|
||||
const u32 packed = static_cast<u32>(src[o + 0]) | (static_cast<u32>(src[o + 1]) << 8) |
|
||||
(static_cast<u32>(src[o + 2]) << 16) |
|
||||
(static_cast<u32>(src[o + 3]) << 24);
|
||||
const u32 b = (packed >> 0) & 0x3FF;
|
||||
const u32 g = (packed >> 10) & 0x3FF;
|
||||
const u32 r = (packed >> 20) & 0x3FF;
|
||||
|
||||
if (readback.hdr_encoded) {
|
||||
// Rec.2020 + PQ. Convert to SDR Rec.709 for PNG output.
|
||||
const float r2020 = pq_decode_lut[r];
|
||||
const float g2020 = pq_decode_lut[g];
|
||||
const float b2020 = pq_decode_lut[b];
|
||||
|
||||
const float r709_nits = 1.6605f * r2020 - 0.5876f * g2020 - 0.0728f * b2020;
|
||||
const float g709_nits = -0.1246f * r2020 + 1.1329f * g2020 - 0.0083f * b2020;
|
||||
const float b709_nits = -0.0182f * r2020 - 0.1006f * g2020 + 1.1187f * b2020;
|
||||
|
||||
const float r_srgb = LinearToSrgb(ToneMapToSdrLinear(r709_nits));
|
||||
const float g_srgb = LinearToSrgb(ToneMapToSdrLinear(g709_nits));
|
||||
const float b_srgb = LinearToSrgb(ToneMapToSdrLinear(b709_nits));
|
||||
|
||||
out_rgba[o + 0] = static_cast<u8>(std::clamp(r_srgb, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
out_rgba[o + 1] = static_cast<u8>(std::clamp(g_srgb, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
out_rgba[o + 2] = static_cast<u8>(std::clamp(b_srgb, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
} else {
|
||||
out_rgba[o + 0] = unorm10_to_u8[r];
|
||||
out_rgba[o + 1] = unorm10_to_u8[g];
|
||||
out_rgba[o + 2] = unorm10_to_u8[b];
|
||||
}
|
||||
out_rgba[o + 3] = 255;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case vk::Format::eA2B10G10R10UnormPack32: {
|
||||
const auto& pq_decode_lut = GetPqDecodeNitsLut();
|
||||
const auto& unorm10_to_u8 = GetUnorm10ToU8Lut();
|
||||
|
||||
for (u64 i = 0; i < pixel_count; ++i) {
|
||||
const size_t o = static_cast<size_t>(i) * 4;
|
||||
const u32 packed = static_cast<u32>(src[o + 0]) | (static_cast<u32>(src[o + 1]) << 8) |
|
||||
(static_cast<u32>(src[o + 2]) << 16) |
|
||||
(static_cast<u32>(src[o + 3]) << 24);
|
||||
const u32 r = (packed >> 0) & 0x3FF;
|
||||
const u32 g = (packed >> 10) & 0x3FF;
|
||||
const u32 b = (packed >> 20) & 0x3FF;
|
||||
|
||||
if (readback.hdr_encoded) {
|
||||
// HDR swapchain path is Rec.2020 + PQ. Convert to SDR Rec.709 for PNG output.
|
||||
const float r2020 = pq_decode_lut[r];
|
||||
const float g2020 = pq_decode_lut[g];
|
||||
const float b2020 = pq_decode_lut[b];
|
||||
|
||||
const float r709_nits = 1.6605f * r2020 - 0.5876f * g2020 - 0.0728f * b2020;
|
||||
const float g709_nits = -0.1246f * r2020 + 1.1329f * g2020 - 0.0083f * b2020;
|
||||
const float b709_nits = -0.0182f * r2020 - 0.1006f * g2020 + 1.1187f * b2020;
|
||||
|
||||
const float r_srgb = LinearToSrgb(ToneMapToSdrLinear(r709_nits));
|
||||
const float g_srgb = LinearToSrgb(ToneMapToSdrLinear(g709_nits));
|
||||
const float b_srgb = LinearToSrgb(ToneMapToSdrLinear(b709_nits));
|
||||
|
||||
out_rgba[o + 0] = static_cast<u8>(std::clamp(r_srgb, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
out_rgba[o + 1] = static_cast<u8>(std::clamp(g_srgb, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
out_rgba[o + 2] = static_cast<u8>(std::clamp(b_srgb, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
} else {
|
||||
out_rgba[o + 0] = unorm10_to_u8[r];
|
||||
out_rgba[o + 1] = unorm10_to_u8[g];
|
||||
out_rgba[o + 2] = unorm10_to_u8[b];
|
||||
}
|
||||
out_rgba[o + 3] = 255;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
LOG_WARNING(Render_Vulkan, "Unsupported screenshot format: {}",
|
||||
vk::to_string(readback.format));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool WritePng(const std::filesystem::path& path, const std::span<const u8> rgba,
|
||||
const u32 width, const u32 height) {
|
||||
Common::FS::IOFile file(path, Common::FS::FileAccessMode::Create);
|
||||
if (!file.IsOpen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
||||
if (png_ptr == nullptr) {
|
||||
return false;
|
||||
}
|
||||
png_infop info_ptr = png_create_info_struct(png_ptr);
|
||||
if (info_ptr == nullptr) {
|
||||
png_destroy_write_struct(&png_ptr, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (setjmp(png_jmpbuf(png_ptr)) != 0) {
|
||||
png_destroy_write_struct(&png_ptr, &info_ptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
png_init_io(png_ptr, file.file);
|
||||
png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
|
||||
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
|
||||
png_write_info(png_ptr, info_ptr);
|
||||
|
||||
thread_local std::vector<png_bytep> rows;
|
||||
rows.resize(height);
|
||||
for (u32 y = 0; y < height; ++y) {
|
||||
rows[y] = const_cast<png_bytep>(rgba.data() + static_cast<size_t>(y) * width * 4);
|
||||
}
|
||||
|
||||
png_write_image(png_ptr, rows.data());
|
||||
png_write_end(png_ptr, info_ptr);
|
||||
png_destroy_write_struct(&png_ptr, &info_ptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void SavePendingScreenshots(const std::vector<ScreenshotReadback>& readbacks) {
|
||||
for (const auto& readback : readbacks) {
|
||||
if (readback.paths.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<u8> rgba;
|
||||
if (!ConvertReadbackToRgba8(readback, rgba)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& primary_path = readback.paths.front();
|
||||
if (!WritePng(primary_path, rgba, readback.width, readback.height)) {
|
||||
LOG_ERROR(Render_Vulkan, "Failed saving screenshot to {}", primary_path.string());
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG_INFO(Render_Vulkan, "Saved screenshot: {}", primary_path.string());
|
||||
|
||||
for (size_t i = 1; i < readback.paths.size(); ++i) {
|
||||
const auto& path = readback.paths[i];
|
||||
std::error_code ec{};
|
||||
std::filesystem::copy_file(primary_path, path, std::filesystem::copy_options::none, ec);
|
||||
if (ec) {
|
||||
// Fallback for platforms/filesystems where copy_file can fail for transient
|
||||
// reasons.
|
||||
if (!WritePng(path, rgba, readback.width, readback.height)) {
|
||||
LOG_ERROR(Render_Vulkan, "Failed saving screenshot to {}", path.string());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO(Render_Vulkan, "Saved screenshot: {}", path.string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Presenter::Presenter(Frontend::WindowSDL& window_, AmdGpu::Liverpool* liverpool_)
|
||||
: window{window_}, liverpool{liverpool_},
|
||||
instance{window, EmulatorSettings.GetGpuId(), EmulatorSettings.IsVkValidationEnabled(),
|
||||
@ -338,11 +707,32 @@ Frame* Presenter::PrepareFrame(const Libraries::VideoOut::BufferAttributeGroup&
|
||||
|
||||
auto& image = texture_cache.GetImage(image_id);
|
||||
auto image_view = *image.FindView(view_info).image_view;
|
||||
image.Transit(vk::ImageLayout::eShaderReadOnlyOptimal, vk::AccessFlagBits2::eShaderRead, {});
|
||||
|
||||
const vk::Extent2D image_size = {image.info.size.width, image.info.size.height};
|
||||
expected_ratio = static_cast<float>(image_size.width) / static_cast<float>(image_size.height);
|
||||
|
||||
const u32 capture_game_only_count = VideoCore::ConsumeGameOnlyScreenshotRequests();
|
||||
std::vector<ScreenshotReadback> pending_screenshots;
|
||||
if (capture_game_only_count > 0) {
|
||||
pending_screenshots.reserve(1);
|
||||
const bool hdr_encoded =
|
||||
attribute.attrib.pixel_format == Libraries::VideoOut::PixelFormat::A2R10G10B10Bt2020Pq;
|
||||
pending_screenshots.emplace_back(
|
||||
instance, draw_scheduler, ScreenshotKind::GameOnly,
|
||||
BuildScreenshotPaths(ScreenshotKind::GameOnly, capture_game_only_count),
|
||||
image_size.width, image_size.height, view_info.format, hdr_encoded);
|
||||
auto& readback = pending_screenshots.back();
|
||||
|
||||
// Capture the guest output before any host-side scaling (FSR/PP) is applied.
|
||||
image.Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead, {},
|
||||
cmdbuf);
|
||||
CopyImageToReadback(cmdbuf, image.GetImage(), vk::ImageLayout::eTransferSrcOptimal,
|
||||
readback);
|
||||
}
|
||||
|
||||
// Continue with host-side passes that draw the displayed (scaled) frame.
|
||||
image.Transit(vk::ImageLayout::eShaderReadOnlyOptimal, vk::AccessFlagBits2::eShaderRead, {},
|
||||
cmdbuf);
|
||||
|
||||
image_view = fsr_pass.Render(cmdbuf, image_view, image_size, {frame->width, frame->height},
|
||||
fsr_settings, frame->is_hdr);
|
||||
pp_pass.Render(cmdbuf, image_view, image_size, *frame, pp_settings);
|
||||
@ -350,6 +740,14 @@ Frame* Presenter::PrepareFrame(const Libraries::VideoOut::BufferAttributeGroup&
|
||||
DebugState.game_resolution = {image_size.width, image_size.height};
|
||||
DebugState.output_resolution = {frame->width, frame->height};
|
||||
|
||||
std::shared_ptr<std::vector<ScreenshotReadback>> deferred_screenshots{};
|
||||
if (!pending_screenshots.empty()) {
|
||||
deferred_screenshots =
|
||||
std::make_shared<std::vector<ScreenshotReadback>>(std::move(pending_screenshots));
|
||||
draw_scheduler.DeferPriorityOperation(
|
||||
[deferred_screenshots]() { SavePendingScreenshots(*deferred_screenshots); });
|
||||
}
|
||||
|
||||
// Flush frame creation commands.
|
||||
frame->ready_semaphore = draw_scheduler.GetMasterSemaphore()->Handle();
|
||||
frame->ready_tick = draw_scheduler.CurrentTick();
|
||||
@ -471,6 +869,11 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame) {
|
||||
|
||||
auto& scheduler = present_scheduler;
|
||||
const auto cmdbuf = scheduler.CommandBuffer();
|
||||
const u32 capture_with_overlays_count = VideoCore::ConsumeWithOverlaysScreenshotRequests();
|
||||
std::vector<ScreenshotReadback> pending_screenshots;
|
||||
if (capture_with_overlays_count > 0) {
|
||||
pending_screenshots.reserve(1);
|
||||
}
|
||||
|
||||
if (EmulatorSettings.IsVkHostMarkersEnabled()) {
|
||||
cmdbuf.beginDebugUtilsLabelEXT(vk::DebugUtilsLabelEXT{
|
||||
@ -519,22 +922,7 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame) {
|
||||
},
|
||||
};
|
||||
|
||||
const vk::ImageMemoryBarrier post_barrier{
|
||||
.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite,
|
||||
.dstAccessMask = vk::AccessFlagBits::eMemoryRead,
|
||||
.oldLayout = vk::ImageLayout::eColorAttachmentOptimal,
|
||||
.newLayout = vk::ImageLayout::ePresentSrcKHR,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = swapchain_image,
|
||||
.subresourceRange{
|
||||
.aspectMask = vk::ImageAspectFlagBits::eColor,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = 1,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = VK_REMAINING_ARRAY_LAYERS,
|
||||
},
|
||||
};
|
||||
bool swapchain_copied_for_screenshot = false;
|
||||
|
||||
cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput,
|
||||
vk::PipelineStageFlagBits::eColorAttachmentOutput,
|
||||
@ -594,6 +982,63 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame) {
|
||||
}
|
||||
ImGui::Core::Render(cmdbuf, swapchain_image_view, swapchain.GetExtent());
|
||||
|
||||
if (capture_with_overlays_count > 0) {
|
||||
pending_screenshots.emplace_back(
|
||||
instance, scheduler, ScreenshotKind::WithOverlays,
|
||||
BuildScreenshotPaths(ScreenshotKind::WithOverlays, capture_with_overlays_count),
|
||||
extent.width, extent.height,
|
||||
swapchain.GetHDR() ? vk::Format::eA2B10G10R10UnormPack32
|
||||
: swapchain.GetSurfaceFormat().format,
|
||||
swapchain.GetHDR());
|
||||
auto& readback = pending_screenshots.back();
|
||||
|
||||
const vk::ImageMemoryBarrier to_transfer{
|
||||
.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite,
|
||||
.dstAccessMask = vk::AccessFlagBits::eTransferRead,
|
||||
.oldLayout = vk::ImageLayout::eColorAttachmentOptimal,
|
||||
.newLayout = vk::ImageLayout::eTransferSrcOptimal,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = swapchain_image,
|
||||
.subresourceRange{
|
||||
.aspectMask = vk::ImageAspectFlagBits::eColor,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = 1,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = VK_REMAINING_ARRAY_LAYERS,
|
||||
},
|
||||
};
|
||||
|
||||
cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput,
|
||||
vk::PipelineStageFlagBits::eTransfer,
|
||||
vk::DependencyFlagBits::eByRegion, {}, {}, to_transfer);
|
||||
CopyImageToReadback(cmdbuf, swapchain_image, vk::ImageLayout::eTransferSrcOptimal,
|
||||
readback);
|
||||
swapchain_copied_for_screenshot = true;
|
||||
}
|
||||
|
||||
const vk::AccessFlags post_src_access_mask =
|
||||
swapchain_copied_for_screenshot ? vk::AccessFlagBits::eTransferRead
|
||||
: vk::AccessFlagBits::eColorAttachmentWrite;
|
||||
const vk::ImageLayout post_old_layout = swapchain_copied_for_screenshot
|
||||
? vk::ImageLayout::eTransferSrcOptimal
|
||||
: vk::ImageLayout::eColorAttachmentOptimal;
|
||||
const vk::ImageMemoryBarrier post_barrier{
|
||||
.srcAccessMask = post_src_access_mask,
|
||||
.dstAccessMask = vk::AccessFlagBits::eMemoryRead,
|
||||
.oldLayout = post_old_layout,
|
||||
.newLayout = vk::ImageLayout::ePresentSrcKHR,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = swapchain_image,
|
||||
.subresourceRange{
|
||||
.aspectMask = vk::ImageAspectFlagBits::eColor,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = 1,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = VK_REMAINING_ARRAY_LAYERS,
|
||||
},
|
||||
};
|
||||
cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eAllCommands,
|
||||
vk::PipelineStageFlagBits::eAllCommands,
|
||||
vk::DependencyFlagBits::eByRegion, {}, {}, post_barrier);
|
||||
@ -607,6 +1052,14 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame) {
|
||||
}
|
||||
|
||||
// Flush vulkan commands.
|
||||
std::shared_ptr<std::vector<ScreenshotReadback>> deferred_screenshots{};
|
||||
if (!pending_screenshots.empty()) {
|
||||
deferred_screenshots =
|
||||
std::make_shared<std::vector<ScreenshotReadback>>(std::move(pending_screenshots));
|
||||
scheduler.DeferPriorityOperation(
|
||||
[deferred_screenshots]() { SavePendingScreenshots(*deferred_screenshots); });
|
||||
}
|
||||
|
||||
SubmitInfo info{};
|
||||
info.AddWait(swapchain.GetImageAcquiredSemaphore());
|
||||
info.AddWait(frame->ready_semaphore, frame->ready_tick);
|
||||
@ -615,9 +1068,11 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame) {
|
||||
scheduler.Flush(info);
|
||||
|
||||
// Present to swapchain.
|
||||
std::scoped_lock submit_lock{Scheduler::submit_mutex};
|
||||
if (!swapchain.Present()) {
|
||||
swapchain.Recreate(window.GetWidth(), window.GetHeight());
|
||||
{
|
||||
std::scoped_lock submit_lock{Scheduler::submit_mutex};
|
||||
if (!swapchain.Present()) {
|
||||
swapchain.Recreate(window.GetWidth(), window.GetHeight());
|
||||
}
|
||||
}
|
||||
|
||||
free_frame();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user