diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index bdc7b36711..def03dbd80 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -280,7 +280,7 @@ if(NOT OPENSSL_FOUND) WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/external/openssl") endif() - set(OPENSSL_ROOT_DIR "${CMAKE_BINARY_DIR}/external/openssl/openssl-3/x64") + set(OPENSSL_ROOT_DIR "${CMAKE_BINARY_DIR}/external/openssl/openssl-3.1/x64") endif() find_package(OpenSSL REQUIRED) diff --git a/vita3k/CMakeLists.txt b/vita3k/CMakeLists.txt index 87deaff48a..04177f04ca 100644 --- a/vita3k/CMakeLists.txt +++ b/vita3k/CMakeLists.txt @@ -274,8 +274,8 @@ elseif(WIN32) add_custom_command( TARGET vita3k POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_BINARY_DIR}/external/openssl/openssl-3/x64/bin/libssl-3-x64.dll" "$" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_BINARY_DIR}/external/openssl/openssl-3/x64/bin/libcrypto-3-x64.dll" "$") + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_BINARY_DIR}/external/openssl/openssl-3.1/x64/bin/libssl-3-x64.dll" "$" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_BINARY_DIR}/external/openssl/openssl-3.1/x64/bin/libcrypto-3-x64.dll" "$") endif() endif() diff --git a/vita3k/config/include/config/config.h b/vita3k/config/include/config/config.h index 1a35576d2c..f51b864579 100644 --- a/vita3k/config/include/config/config.h +++ b/vita3k/config/include/config/config.h @@ -132,6 +132,7 @@ enum PerfomanceOverleyPosition { code(int, "keyboard-gui-fullscreen", 68, keyboard_gui_fullscreen) \ code(int, "keyboard-gui-toggle-touch", 23, keyboard_gui_toggle_touch) \ code(int, "keyboard-toggle-texture-replacement", 0, keyboard_toggle_texture_replacement) \ + code(int, "keyboard-take-screenshot", 0, keyboard_take_screenshot) \ code(std::string, "user-id", std::string{}, user_id) \ code(bool, "user-auto-connect", false, auto_user_login) \ code(std::string, "user-lang", std::string{}, user_lang) \ diff --git a/vita3k/gui/src/controls_dialog.cpp b/vita3k/gui/src/controls_dialog.cpp index 631fa033a5..c6830229dc 100644 --- a/vita3k/gui/src/controls_dialog.cpp +++ b/vita3k/gui/src/controls_dialog.cpp @@ -97,6 +97,7 @@ static void remapper_button(GuiState &gui, EmuEnvState &emuenv, int *button, con ImGui::TableSetColumnIndex(1); // the association of the key int key_association = *button; + ImGui::PushID(button_name); if (ImGui::Button(SDL_key_to_string[key_association])) { gui.old_captured_key = key_association; gui.is_capturing_keys = true; @@ -117,6 +118,7 @@ static void remapper_button(GuiState &gui, EmuEnvState &emuenv, int *button, con } config::serialize_config(emuenv.cfg, emuenv.cfg.config_path); } + ImGui::PopID(); } void draw_controls_dialog(GuiState &gui, EmuEnvState &emuenv) { @@ -195,6 +197,7 @@ void draw_controls_dialog(GuiState &gui, EmuEnvState &emuenv) { ImGui::TableSetupColumn("button"); ImGui::TableSetupColumn("mapped_button"); remapper_button(gui, emuenv, &emuenv.cfg.keyboard_toggle_texture_replacement, lang["toggle_texture_replacement"].c_str()); + remapper_button(gui, emuenv, &emuenv.cfg.keyboard_take_screenshot, "Take a screenshot"); ImGui::EndTable(); } diff --git a/vita3k/interface.cpp b/vita3k/interface.cpp index 4c90624935..708c14b2d8 100644 --- a/vita3k/interface.cpp +++ b/vita3k/interface.cpp @@ -50,6 +50,8 @@ #include #include +#include +#include #include @@ -530,6 +532,33 @@ static void toggle_texture_replacement(EmuEnvState &emuenv) { emuenv.renderer->get_texture_cache()->set_replacement_state(emuenv.cfg.current_config.import_textures, emuenv.cfg.current_config.export_textures, emuenv.cfg.current_config.export_as_png); } +static void take_screenshot(EmuEnvState &emuenv) { + if (emuenv.io.title_id.empty()) { + LOG_ERROR("Trying to take a screenshot while not ingame"); + } + + uint32_t width, height; + std::vector frame = emuenv.renderer->dump_frame(emuenv.display, width, height); + + if (frame.empty() || frame.size() != width * height) { + LOG_ERROR("Failed to take screenshot"); + return; + } + + // set the alpha to 1 + for (int i = 0; i < width * height; i++) + frame[i] |= 0xFF000000; + + const fs::path save_folder = emuenv.shared_path / "screenshots"; + fs::create_directories(save_folder); + + const fs::path save_file = save_folder / fmt::format("{}_{:%Y-%m-%d_%H-%M-%OS}.png", emuenv.io.title_id, fmt::localtime(std::time(nullptr))); + if (stbi_write_png(fs_utils::path_to_utf8(save_file).c_str(), width, height, 4, frame.data(), width * 4) == 1) + LOG_INFO("Successfully saved screenshot to {}", save_file); + else + LOG_INFO("Failed to save screenshot"); +} + bool handle_events(EmuEnvState &emuenv, GuiState &gui) { refresh_controllers(emuenv.ctrl, emuenv); const auto allow_switch_state = !emuenv.io.title_id.empty() && !gui.vita_area.app_close && !gui.vita_area.home_screen && !gui.vita_area.user_management && !gui.configuration_menu.custom_settings_dialog && !gui.configuration_menu.settings_dialog && !gui.controls_menu.controls_dialog && gui::get_sys_apps_state(gui); @@ -681,6 +710,8 @@ bool handle_events(EmuEnvState &emuenv, GuiState &gui) { switch_full_screen(emuenv); if (event.key.keysym.scancode == emuenv.cfg.keyboard_toggle_texture_replacement && !gui.is_key_capture_dropped) toggle_texture_replacement(emuenv); + if (event.key.keysym.scancode == emuenv.cfg.keyboard_take_screenshot && !gui.is_key_capture_dropped) + take_screenshot(emuenv); if (sce_ctrl_btn != 0) ui_navigation(sce_ctrl_btn); diff --git a/vita3k/lang/src/lang.cpp b/vita3k/lang/src/lang.cpp index 85f2f8dd26..17633edab6 100644 --- a/vita3k/lang/src/lang.cpp +++ b/vita3k/lang/src/lang.cpp @@ -378,7 +378,7 @@ void init_lang(LangState &lang, EmuEnvState &emuenv) { set_lang_string(lang.settings_dialog.gpu, settings_dialog.child("gpu")); // Audio - set_lang_string(lang.settings_dialog.emulator, settings_dialog.child("audio")); + set_lang_string(lang.settings_dialog.audio, settings_dialog.child("audio")); // System set_lang_string(lang.settings_dialog.system, settings_dialog.child("system")); diff --git a/vita3k/renderer/include/renderer/gl/state.h b/vita3k/renderer/include/renderer/gl/state.h index ac4c29d1bb..5386448393 100644 --- a/vita3k/renderer/include/renderer/gl/state.h +++ b/vita3k/renderer/include/renderer/gl/state.h @@ -54,6 +54,8 @@ struct GLState : public renderer::State { void render_frame(const SceFVector2 &viewport_pos, const SceFVector2 &viewport_size, DisplayState &display, const GxmState &gxm, MemState &mem) override; void swap_window(SDL_Window *window) override; + std::vector dump_frame(DisplayState &display, uint32_t &width, uint32_t &height) override; + int get_supported_filters() override; void set_screen_filter(const std::string_view &filter) override; int get_max_anisotropic_filtering() override; diff --git a/vita3k/renderer/include/renderer/gl/surface_cache.h b/vita3k/renderer/include/renderer/gl/surface_cache.h index d33187e3aa..f892a7a875 100644 --- a/vita3k/renderer/include/renderer/gl/surface_cache.h +++ b/vita3k/renderer/include/renderer/gl/surface_cache.h @@ -18,7 +18,9 @@ #pragma once #include -#include +#include +#include +#include #include #include @@ -28,9 +30,18 @@ struct MemState; -namespace renderer::gl { +namespace renderer { +struct State; + +namespace gl { + struct GLRenderTarget; +enum SurfaceTextureRetrievePurpose { + READING, + WRITING, +}; + struct GLSurfaceCacheInfo { enum { FLAG_DIRTY = 1 << 0, @@ -76,7 +87,7 @@ struct GLDepthStencilSurfaceCacheInfo : public GLSurfaceCacheInfo { std::int32_t height; }; -class GLSurfaceCache : public SurfaceCache { +class GLSurfaceCache { private: static constexpr std::uint32_t MAX_CACHE_SIZE_PER_CONTAINER = 20; @@ -117,5 +128,7 @@ class GLSurfaceCache : public SurfaceCache { } GLuint sourcing_color_surface_for_presentation(Ptr address, uint32_t width, uint32_t height, const std::uint32_t pitch, float *uvs, const int res_multiplier, SceFVector2 &texture_size); + std::vector dump_frame(Ptr address, uint32_t width, uint32_t height, uint32_t pitch, int res_multiplier, bool support_get_texture_sub_image); }; -} // namespace renderer::gl +} // namespace gl +} // namespace renderer diff --git a/vita3k/renderer/include/renderer/state.h b/vita3k/renderer/include/renderer/state.h index 72b71c02a5..cce86cb992 100644 --- a/vita3k/renderer/include/renderer/state.h +++ b/vita3k/renderer/include/renderer/state.h @@ -90,6 +90,8 @@ struct State { const GxmState &gxm, MemState &mem) = 0; virtual void swap_window(SDL_Window *window) = 0; + // perform a screenshot of the (upscaled) frame to be rendered and return it in a vector in its rgba8 format + virtual std::vector dump_frame(DisplayState &display, uint32_t &width, uint32_t &height) = 0; // return a mask of the features which can influence the compiled shaders virtual uint32_t get_features_mask() { return 0; diff --git a/vita3k/renderer/include/renderer/surface_cache.h b/vita3k/renderer/include/renderer/surface_cache.h deleted file mode 100644 index d745b69a71..0000000000 --- a/vita3k/renderer/include/renderer/surface_cache.h +++ /dev/null @@ -1,38 +0,0 @@ -// Vita3K emulator project -// Copyright (C) 2024 Vita3K team -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -#pragma once - -#include - -#include -#include -#include - -struct MemState; - -namespace renderer { - -struct State; - -enum SurfaceTextureRetrievePurpose { - READING, - WRITING, -}; - -class SurfaceCache {}; -} // namespace renderer diff --git a/vita3k/renderer/include/renderer/vulkan/state.h b/vita3k/renderer/include/renderer/vulkan/state.h index 2ae3f9143b..9e31aa5d57 100644 --- a/vita3k/renderer/include/renderer/vulkan/state.h +++ b/vita3k/renderer/include/renderer/vulkan/state.h @@ -111,6 +111,8 @@ struct VKState : public renderer::State { void render_frame(const SceFVector2 &viewport_pos, const SceFVector2 &viewport_size, DisplayState &display, const GxmState &gxm, MemState &mem) override; void swap_window(SDL_Window *window) override; + std::vector dump_frame(DisplayState &display, uint32_t &width, uint32_t &height) override; + uint32_t get_features_mask() override; int get_supported_filters() override; void set_screen_filter(const std::string_view &filter) override; diff --git a/vita3k/renderer/include/renderer/vulkan/surface_cache.h b/vita3k/renderer/include/renderer/vulkan/surface_cache.h index df03367d6c..13a85ab1ed 100644 --- a/vita3k/renderer/include/renderer/vulkan/surface_cache.h +++ b/vita3k/renderer/include/renderer/vulkan/surface_cache.h @@ -17,8 +17,9 @@ #pragma once -#include - +#include +#include +#include #include #include @@ -145,7 +146,7 @@ struct SurfaceRetrieveResult { vkutil::Image *base_image; }; -class VKSurfaceCache : public SurfaceCache { +class VKSurfaceCache { private: VKState &state; @@ -207,6 +208,10 @@ class VKSurfaceCache : public SurfaceCache { // Viewport should already have its fields width and height filled vk::ImageView sourcing_color_surface_for_presentation(Ptr address, uint32_t pitch, Viewport &viewport); + // Dump an rgba8 frame with the given properties to the returned vector + // if this function fails, the vector will be empty + std::vector dump_frame(Ptr address, uint32_t width, uint32_t height, uint32_t pitch); + void set_render_target(VKRenderTarget *new_target) { target = new_target; } diff --git a/vita3k/renderer/src/gl/renderer.cpp b/vita3k/renderer/src/gl/renderer.cpp index 26df1ba474..551080f9b9 100644 --- a/vita3k/renderer/src/gl/renderer.cpp +++ b/vita3k/renderer/src/gl/renderer.cpp @@ -534,7 +534,7 @@ void lookup_and_get_surface_data(GLState &renderer, MemState &mem, SceGxmColorSu GLint tex_handle = static_cast(renderer.surface_cache.retrieve_color_surface_texture_handle(renderer, static_cast(surface.width), static_cast(surface.height), static_cast(surface.strideInPixels), - gxm::get_base_format(surface.colorFormat), surface.data, renderer::SurfaceTextureRetrievePurpose::READING, swizzle)); + gxm::get_base_format(surface.colorFormat), surface.data, SurfaceTextureRetrievePurpose::READING, swizzle)); if (tex_handle == 0) { return; @@ -716,6 +716,18 @@ void GLState::swap_window(SDL_Window *window) { SDL_GL_SwapWindow(window); } +std::vector GLState::dump_frame(DisplayState &display, uint32_t &width, uint32_t &height) { + DisplayFrameInfo frame; + { + std::lock_guard guard(display.display_info_mutex); + frame = display.next_rendered_frame; + } + + width = frame.image_size.x * res_multiplier; + height = frame.image_size.y * res_multiplier; + return surface_cache.dump_frame(frame.base, width, height, frame.pitch, res_multiplier, features.support_get_texture_sub_image); +} + int GLState::get_supported_filters() { // actually it's not even bilinear, it's either bilinear or nearest depending on the last use of the texture.. // TODO: add bicubic filter and allow disabling bilinear. diff --git a/vita3k/renderer/src/gl/surface_cache.cpp b/vita3k/renderer/src/gl/surface_cache.cpp index 521bb42bba..5c7008a320 100644 --- a/vita3k/renderer/src/gl/surface_cache.cpp +++ b/vita3k/renderer/src/gl/surface_cache.cpp @@ -649,7 +649,7 @@ GLuint GLSurfaceCache::retrieve_framebuffer_handle(const State &state, const Mem std::uint32_t swizzle_set = color->colorFormat & SCE_GXM_COLOR_SWIZZLE_MASK; color_handle = static_cast(retrieve_color_surface_texture_handle(state, color->width, color->height, color->strideInPixels, gxm::get_base_format(color->colorFormat), color->data, - renderer::SurfaceTextureRetrievePurpose::WRITING, swizzle_set, stored_height)); + SurfaceTextureRetrievePurpose::WRITING, swizzle_set, stored_height)); } else { color_handle = target->attachments[0]; } @@ -758,4 +758,47 @@ GLuint GLSurfaceCache::sourcing_color_surface_for_presentation(Ptr a return 0; } +std::vector GLSurfaceCache::dump_frame(Ptr address, uint32_t width, uint32_t height, uint32_t pitch, int res_multiplier, bool support_get_texture_sub_image) { + auto ite = color_surface_textures.lower_bound(address.address()); + if (ite == color_surface_textures.end() || ite->second->pixel_stride != pitch) { + return {}; + } + + const GLColorSurfaceCacheInfo &info = *ite->second; + + const uint32_t data_delta = address.address() - ite->first; + const uint32_t pitch_byte = pitch * 4; + if (info.pixel_stride != pitch || data_delta % pitch_byte != 0) + return {}; + + const uint32_t line_delta = (data_delta / pitch_byte) * res_multiplier; + if (line_delta >= info.height) + return {}; + + if (!support_get_texture_sub_image && (line_delta != 0 || info.width != width || info.height != height)) { + LOG_ERROR("Dumping this frame is not supported on the OpenGL renderer"); + return {}; + } + + const uint32_t real_height = std::min(height, info.height - line_delta); + + std::vector frame(width * height, 0); + glPixelStorei(GL_PACK_ROW_LENGTH, 0); + + // retrieve the texture, it is on the GPU right now + if (support_get_texture_sub_image) { + glGetTextureSubImage(info.gl_texture[0], 0, 0, line_delta, 0, width, real_height, 1, GL_RGBA, GL_UNSIGNED_BYTE, frame.size() * 4, frame.data()); + } else { + GLint last_texture = 0; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture); + + glBindTexture(GL_TEXTURE_2D, info.gl_texture[0]); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, frame.data()); + + glBindTexture(GL_TEXTURE_2D, last_texture); + } + + return frame; +} + } // namespace renderer::gl diff --git a/vita3k/renderer/src/gl/sync_state.cpp b/vita3k/renderer/src/gl/sync_state.cpp index 353f797c73..e919156b15 100644 --- a/vita3k/renderer/src/gl/sync_state.cpp +++ b/vita3k/renderer/src/gl/sync_state.cpp @@ -337,7 +337,7 @@ void sync_texture(GLState &state, GLContext &context, MemState &mem, std::size_t texture_as_surface = state.surface_cache.retrieve_color_surface_texture_handle( state, width, height, stride_in_pixels, format_target_of_texture, Ptr(data_addr), - renderer::SurfaceTextureRetrievePurpose::READING, swizz_raw); + SurfaceTextureRetrievePurpose::READING, swizz_raw); swizzle_surface = color::translate_swizzle(static_cast(format_target_of_texture | swizz_raw)); only_nearest = color::is_write_surface_non_linearity_filtering(format_target_of_texture); diff --git a/vita3k/renderer/src/texture/replacement.cpp b/vita3k/renderer/src/texture/replacement.cpp index 1415c814f1..19c93720df 100644 --- a/vita3k/renderer/src/texture/replacement.cpp +++ b/vita3k/renderer/src/texture/replacement.cpp @@ -381,7 +381,7 @@ void TextureCache::export_texture_impl(SceGxmTextureBaseFormat base_format, uint uint64_t hash = current_info->hash; const std::string file_name = fmt::format("{:016X}.png", hash); fs::path export_name = export_folder / file_name; - stbi_write_png(export_name.generic_string().c_str(), width, height, nb_comp, data, pixels_per_stride * nb_comp); + stbi_write_png(fs_utils::path_to_utf8(export_name).c_str(), width, height, nb_comp, data, pixels_per_stride * nb_comp); if (log_texture_export) LOG_DEBUG("Texture {} ({}x{}) exported", file_name, width, height); @@ -507,7 +507,7 @@ bool TextureCache::import_configure_texture() { imported_texture_decoded = imported_texture_raw_data.data() + dds_descriptor->headerSize; } else { int nb_channels; - imported_texture_decoded = stbi_load(import_name.generic_string().c_str(), reinterpret_cast(&width), reinterpret_cast(&height), &nb_channels, nb_comp); + imported_texture_decoded = stbi_load(fs_utils::path_to_utf8(import_name).c_str(), reinterpret_cast(&width), reinterpret_cast(&height), &nb_channels, nb_comp); if (imported_texture_decoded == nullptr) { LOG_ERROR("Failed to decode {}", file_name); return false; diff --git a/vita3k/renderer/src/vulkan/renderer.cpp b/vita3k/renderer/src/vulkan/renderer.cpp index c2e1811e3c..fe985b3760 100644 --- a/vita3k/renderer/src/vulkan/renderer.cpp +++ b/vita3k/renderer/src/vulkan/renderer.cpp @@ -49,6 +49,7 @@ static VKAPI_ATTR VkBool32 VKAPI_CALL debug_callback( "VUID-VkImageCreateInfo-imageCreateMaxMipLevels-02251", // srgb does not support the storage format "VUID-vkCmdPipelineBarrier-pDependencies-02285", // shader write -> vertex input read self-dependency, wrong error "VUID-vkCmdDrawIndexed-None-09003", // reading from color attachment, works on most GPUs with a general layout + "VUID-vkAcquireNextImageKHR-semaphore-01779" // Semaphore misuse, to fix }; if (message_severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT @@ -747,6 +748,18 @@ void VKState::swap_window(SDL_Window *window) { } } +std::vector VKState::dump_frame(DisplayState &display, uint32_t &width, uint32_t &height) { + DisplayFrameInfo frame; + { + std::lock_guard guard(display.display_info_mutex); + frame = display.next_rendered_frame; + } + + width = frame.image_size.x * res_multiplier; + height = frame.image_size.y * res_multiplier; + return surface_cache.dump_frame(frame.base, width, height, frame.pitch); +} + uint32_t VKState::get_features_mask() { union { struct { diff --git a/vita3k/renderer/src/vulkan/surface_cache.cpp b/vita3k/renderer/src/vulkan/surface_cache.cpp index 8dd7977556..0c51cbf8fa 100644 --- a/vita3k/renderer/src/vulkan/surface_cache.cpp +++ b/vita3k/renderer/src/vulkan/surface_cache.cpp @@ -1218,4 +1218,53 @@ vk::ImageView VKSurfaceCache::sourcing_color_surface_for_presentation(Ptr VKSurfaceCache::dump_frame(Ptr address, uint32_t width, uint32_t height, uint32_t pitch) { + // get closest surface with an address below address + auto ite = color_address_lookup.upper_bound(address.address()); + if (ite == color_address_lookup.begin()) { + return {}; + } + ite--; + + const ColorSurfaceCacheInfo &info = *ite->second; + + const uint32_t data_delta = address.address() - ite->first; + const uint32_t pitch_byte = pitch * 4; + if (info.stride_bytes != pitch_byte || data_delta % pitch_byte != 0) + return {}; + + const uint32_t line_delta = (data_delta / pitch_byte) * state.res_multiplier; + if (line_delta >= info.height) + return {}; + + const uint32_t real_height = std::min(height, info.height - line_delta); + + std::vector frame(width * height, 0); + + // we need a temporary buffer and command buffer for this + // this is a raii buffer, it will be destroyed at the end of this function + vkutil::Buffer temp_buff(width * height * 4); + temp_buff.init_buffer(vk::BufferUsageFlagBits::eTransferDst, vkutil::vma_mapped_alloc); + vk::CommandBuffer cmd_buffer = vkutil::create_single_time_command(state.device, state.general_command_pool); + + // layout is general, we can directly copy from it + vk::BufferImageCopy image_copy{ + .bufferOffset = 0, + .bufferRowLength = width, + .bufferImageHeight = height, + .imageSubresource = vkutil::color_subresource_layer, + .imageOffset = { 0, static_cast(line_delta), 0 }, + .imageExtent = { width, real_height, 1 } + }; + cmd_buffer.copyImageToBuffer(info.texture.image, vk::ImageLayout::eGeneral, temp_buff.buffer, image_copy); + + // this will cause a waitIdle, not an issue + vkutil::end_single_time_command(state.device, state.general_queue, state.general_command_pool, cmd_buffer); + + memcpy(frame.data(), temp_buff.mapped_data, frame.size() * 4); + + return frame; +} + +}; // namespace renderer::vulkan