diff --git a/src/core/host_display.cpp b/src/core/host_display.cpp index 6345dabdb..dd9aba03b 100644 --- a/src/core/host_display.cpp +++ b/src/core/host_display.cpp @@ -1,4 +1,12 @@ #include "host_display.h" +#include "common/log.h" +#include "common/string_util.h" +#include "stb_image_resize.h" +#include "stb_image_write.h" +#include +#include +#include +Log_SetChannel(HostDisplay); HostDisplayTexture::~HostDisplayTexture() = default; @@ -46,3 +54,154 @@ std::tuple HostDisplay::CalculateDrawRect() const top += m_display_top_margin; return std::tie(left, top, width, height); } + +bool HostDisplay::WriteTextureToFile(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, + const char* filename, bool clear_alpha /* = true */, bool flip_y /* = false */, + u32 resize_width /* = 0 */, u32 resize_height /* = 0 */) +{ + std::vector texture_data(width * height); + u32 texture_data_stride = sizeof(u32) * width; + if (!DownloadTexture(texture_handle, x, y, width, height, texture_data.data(), texture_data_stride)) + { + Log_ErrorPrintf("Texture download failed"); + return false; + } + + const char* extension = std::strrchr(filename, '.'); + if (!extension) + { + Log_ErrorPrintf("Unable to determine file extension for '%s'", filename); + return false; + } + + if (clear_alpha) + { + for (u32& pixel : texture_data) + pixel |= 0xFF000000; + } + + if (flip_y) + { + std::vector temp(width); + for (u32 flip_row = 0; flip_row < (height / 2); flip_row++) + { + u32* top_ptr = &texture_data[flip_row * width]; + u32* bottom_ptr = &texture_data[((height - 1) - flip_row) * width]; + std::memcpy(temp.data(), top_ptr, texture_data_stride); + std::memcpy(top_ptr, bottom_ptr, texture_data_stride); + std::memcpy(bottom_ptr, temp.data(), texture_data_stride); + } + } + + if (resize_width > 0 && resize_height > 0 && (resize_width != width || resize_height != height)) + { + std::vector resized_texture_data(resize_width * resize_height); + u32 resized_texture_stride = sizeof(u32) * resize_width; + if (!stbir_resize_uint8(reinterpret_cast(texture_data.data()), width, height, texture_data_stride, + reinterpret_cast(resized_texture_data.data()), resize_width, resize_height, + resized_texture_stride, 4)) + { + Log_ErrorPrintf("Failed to resize texture data from %ux%u to %ux%u", width, height, resize_width, resize_height); + return false; + } + + width = resize_width; + height = resize_height; + texture_data = std::move(resized_texture_data); + texture_data_stride = resized_texture_stride; + } + + bool result; + if (StringUtil::Strcasecmp(extension, ".png") == 0) + { + result = (stbi_write_png(filename, width, height, 4, texture_data.data(), texture_data_stride) != 0); + } + else if (StringUtil::Strcasecmp(filename, ".jpg") == 0) + { + result = (stbi_write_jpg(filename, width, height, 4, texture_data.data(), 95) != 0); + } + else if (StringUtil::Strcasecmp(filename, ".tga") == 0) + { + result = (stbi_write_tga(filename, width, height, 4, texture_data.data()) != 0); + } + else if (StringUtil::Strcasecmp(filename, ".bmp") == 0) + { + result = (stbi_write_bmp(filename, width, height, 4, texture_data.data()) != 0); + } + else + { + Log_ErrorPrintf("Unknown extension in filename '%s': '%s'", filename, extension); + return false; + } + + if (!result) + { + Log_ErrorPrintf("Failed to save texture to '%s'", filename); + return false; + } + + return true; +} + +bool HostDisplay::WriteDisplayTextureToFile(const char* filename, bool full_resolution /* = true */, + bool apply_aspect_ratio /* = true */) +{ + if (!m_display_texture_handle) + return false; + + s32 resize_width = 0; + s32 resize_height = 0; + if (apply_aspect_ratio && full_resolution) + { + if (m_display_aspect_ratio > 1.0f) + { + resize_width = m_display_texture_view_width; + resize_height = static_cast(static_cast(resize_width) / m_display_aspect_ratio); + } + else + { + resize_height = m_display_texture_view_height; + resize_width = static_cast(static_cast(resize_height) * m_display_aspect_ratio); + } + } + else if (apply_aspect_ratio) + { + const auto [left, top, right, bottom] = CalculateDrawRect(); + resize_width = right - left; + resize_height = bottom - top; + } + else if (!full_resolution) + { + const auto [left, top, right, bottom] = CalculateDrawRect(); + const float ratio = + static_cast(m_display_texture_view_width) / static_cast(m_display_texture_view_height); + if (ratio > 1.0f) + { + resize_width = right - left; + resize_height = static_cast(static_cast(resize_width) / ratio); + } + else + { + resize_height = bottom - top; + resize_width = static_cast(static_cast(resize_height) * ratio); + } + } + + if (resize_width < 0) + resize_width = 1; + if (resize_height < 0) + resize_height = 1; + + const bool flip_y = (m_display_texture_view_height < 0); + s32 read_height = m_display_texture_view_height; + s32 read_y = m_display_texture_view_y; + if (flip_y) + { + read_height = -m_display_texture_view_height; + read_y = (m_display_texture_height - read_height) - (m_display_texture_height - m_display_texture_view_y); + } + + return WriteTextureToFile(m_display_texture_handle, m_display_texture_view_x, read_y, m_display_texture_view_width, + read_height, filename, true, flip_y, static_cast(resize_width), + static_cast(resize_height)); +} diff --git a/src/core/host_display.h b/src/core/host_display.h index 086909d01..19ad7e573 100644 --- a/src/core/host_display.h +++ b/src/core/host_display.h @@ -46,6 +46,9 @@ public: virtual void UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, const void* data, u32 data_stride) = 0; + virtual bool DownloadTexture(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, void* out_data, + u32 out_data_stride) = 0; + virtual void Render() = 0; virtual void SetVSync(bool enabled) = 0; @@ -90,9 +93,16 @@ public: void SetDisplayLinearFiltering(bool enabled) { m_display_linear_filtering = enabled; } void SetDisplayTopMargin(s32 height) { m_display_top_margin = height; } - // Helper function for computing the draw rectangle in a larger window. + /// Helper function for computing the draw rectangle in a larger window. std::tuple CalculateDrawRect() const; + /// Helper function to save texture data to a PNG. If flip_y is set, the image will be flipped aka OpenGL. + bool WriteTextureToFile(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, const char* filename, + bool clear_alpha = true, bool flip_y = false, u32 resize_width = 0, u32 resize_height = 0); + + /// Helper function to save current display texture to PNG. + bool WriteDisplayTextureToFile(const char* filename, bool full_resolution = true, bool apply_aspect_ratio = true); + protected: s32 m_window_width = 0; s32 m_window_height = 0; diff --git a/src/duckstation-qt/d3d11displaywidget.cpp b/src/duckstation-qt/d3d11displaywidget.cpp index 68667cffb..18ebb4922 100644 --- a/src/duckstation-qt/d3d11displaywidget.cpp +++ b/src/duckstation-qt/d3d11displaywidget.cpp @@ -141,6 +141,24 @@ void D3D11DisplayWidget::UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y } } +bool D3D11DisplayWidget::DownloadTexture(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, + void* out_data, u32 out_data_stride) +{ + ID3D11ShaderResourceView* srv = + const_cast(static_cast(texture_handle)); + ID3D11Resource* srv_resource; + D3D11_SHADER_RESOURCE_VIEW_DESC srv_desc; + srv->GetResource(&srv_resource); + srv->GetDesc(&srv_desc); + + if (!m_readback_staging_texture.EnsureSize(m_context.Get(), width, height, srv_desc.Format, false)) + return false; + + m_readback_staging_texture.CopyFromTexture(m_context.Get(), srv_resource, 0, x, y, 0, 0, width, height); + return m_readback_staging_texture.ReadPixels(m_context.Get(), 0, 0, width, height, out_data_stride / sizeof(u32), + static_cast(out_data)); +} + void D3D11DisplayWidget::SetVSync(bool enabled) { m_vsync = enabled; diff --git a/src/duckstation-qt/d3d11displaywidget.h b/src/duckstation-qt/d3d11displaywidget.h index 06b474b82..803a1c441 100644 --- a/src/duckstation-qt/d3d11displaywidget.h +++ b/src/duckstation-qt/d3d11displaywidget.h @@ -1,4 +1,5 @@ #pragma once +#include "common/d3d11/staging_texture.h" #include "common/d3d11/stream_buffer.h" #include "common/d3d11/texture.h" #include "common/windows_headers.h" @@ -39,6 +40,8 @@ public: u32 initial_data_stride, bool dynamic) override; void UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, const void* texture_data, u32 texture_data_stride) override; + bool DownloadTexture(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, void* out_data, + u32 out_data_stride) override; void SetVSync(bool enabled) override; @@ -74,6 +77,7 @@ private: D3D11::Texture m_display_pixels_texture; D3D11::StreamBuffer m_display_uniform_buffer; + D3D11::AutoStagingTexture m_readback_staging_texture; bool m_allow_tearing_supported = false; bool m_vsync = false; diff --git a/src/duckstation-qt/opengldisplaywidget.cpp b/src/duckstation-qt/opengldisplaywidget.cpp index 18b12770f..c27965906 100644 --- a/src/duckstation-qt/opengldisplaywidget.cpp +++ b/src/duckstation-qt/opengldisplaywidget.cpp @@ -164,6 +164,24 @@ void OpenGLDisplayWidget::UpdateTexture(HostDisplayTexture* texture, u32 x, u32 glBindTexture(GL_TEXTURE_2D, old_texture_binding); } +bool OpenGLDisplayWidget::DownloadTexture(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, + void* out_data, u32 out_data_stride) +{ + GLint old_alignment = 0, old_row_length = 0; + glGetIntegerv(GL_PACK_ALIGNMENT, &old_alignment); + glGetIntegerv(GL_PACK_ROW_LENGTH, &old_row_length); + glPixelStorei(GL_PACK_ALIGNMENT, sizeof(u32)); + glPixelStorei(GL_PACK_ROW_LENGTH, out_data_stride / sizeof(u32)); + + const GLuint texture = static_cast(reinterpret_cast(texture_handle)); + GL::Texture::GetTextureSubImage(texture, 0, x, y, 0, width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE, + height * out_data_stride, out_data); + + glPixelStorei(GL_PACK_ALIGNMENT, old_alignment); + glPixelStorei(GL_PACK_ROW_LENGTH, old_row_length); + return true; +} + void OpenGLDisplayWidget::SetVSync(bool enabled) { // Window framebuffer has to be bound to call SetSwapInterval. diff --git a/src/duckstation-qt/opengldisplaywidget.h b/src/duckstation-qt/opengldisplaywidget.h index dbee48124..a4820408a 100644 --- a/src/duckstation-qt/opengldisplaywidget.h +++ b/src/duckstation-qt/opengldisplaywidget.h @@ -44,6 +44,8 @@ public: u32 initial_data_stride, bool dynamic) override; void UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, const void* texture_data, u32 texture_data_stride) override; + bool DownloadTexture(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, void* out_data, + u32 out_data_stride) override; void SetVSync(bool enabled) override; diff --git a/src/duckstation-sdl/d3d11_host_display.cpp b/src/duckstation-sdl/d3d11_host_display.cpp index e12a7a8a0..a1765389d 100644 --- a/src/duckstation-sdl/d3d11_host_display.cpp +++ b/src/duckstation-sdl/d3d11_host_display.cpp @@ -168,6 +168,24 @@ void D3D11HostDisplay::UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, } } +bool D3D11HostDisplay::DownloadTexture(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, void* out_data, + u32 out_data_stride) +{ + ID3D11ShaderResourceView* srv = + const_cast(static_cast(texture_handle)); + ID3D11Resource* srv_resource; + D3D11_SHADER_RESOURCE_VIEW_DESC srv_desc; + srv->GetResource(&srv_resource); + srv->GetDesc(&srv_desc); + + if (!m_readback_staging_texture.EnsureSize(m_context.Get(), width, height, srv_desc.Format, false)) + return false; + + m_readback_staging_texture.CopyFromTexture(m_context.Get(), srv_resource, 0, x, y, 0, 0, width, height); + return m_readback_staging_texture.ReadPixels(m_context.Get(), 0, 0, width, height, out_data_stride / sizeof(u32), + static_cast(out_data)); +} + void D3D11HostDisplay::SetVSync(bool enabled) { m_vsync = enabled; diff --git a/src/duckstation-sdl/d3d11_host_display.h b/src/duckstation-sdl/d3d11_host_display.h index 70e47f6a5..820b19b87 100644 --- a/src/duckstation-sdl/d3d11_host_display.h +++ b/src/duckstation-sdl/d3d11_host_display.h @@ -1,5 +1,6 @@ #pragma once #include "common/d3d11/stream_buffer.h" +#include "common/d3d11/staging_texture.h" #include "common/d3d11/texture.h" #include "common/windows_headers.h" #include "core/host_display.h" @@ -31,6 +32,8 @@ public: bool dynamic) override; void UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, const void* data, u32 data_stride) override; + bool DownloadTexture(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, void* out_data, + u32 out_data_stride) override; void SetVSync(bool enabled) override; @@ -63,6 +66,7 @@ private: D3D11::Texture m_display_pixels_texture; D3D11::StreamBuffer m_display_uniform_buffer; + D3D11::AutoStagingTexture m_readback_staging_texture; bool m_allow_tearing_supported = false; bool m_vsync = true; diff --git a/src/duckstation-sdl/opengl_host_display.cpp b/src/duckstation-sdl/opengl_host_display.cpp index 563a55dd8..2d0f6534a 100644 --- a/src/duckstation-sdl/opengl_host_display.cpp +++ b/src/duckstation-sdl/opengl_host_display.cpp @@ -21,7 +21,7 @@ public: GLuint GetGLID() const { return m_id; } static std::unique_ptr Create(u32 width, u32 height, const void* initial_data, - u32 initial_data_stride) + u32 initial_data_stride) { GLuint id; glGenTextures(1, &id); @@ -129,6 +129,24 @@ void OpenGLHostDisplay::UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, glBindTexture(GL_TEXTURE_2D, old_texture_binding); } +bool OpenGLHostDisplay::DownloadTexture(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, void* out_data, + u32 out_data_stride) +{ + GLint old_alignment = 0, old_row_length = 0; + glGetIntegerv(GL_PACK_ALIGNMENT, &old_alignment); + glGetIntegerv(GL_PACK_ROW_LENGTH, &old_row_length); + glPixelStorei(GL_PACK_ALIGNMENT, sizeof(u32)); + glPixelStorei(GL_PACK_ROW_LENGTH, out_data_stride / sizeof(u32)); + + const GLuint texture = static_cast(reinterpret_cast(texture_handle)); + GL::Texture::GetTextureSubImage(texture, 0, x, y, 0, width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE, + height * out_data_stride, out_data); + + glPixelStorei(GL_PACK_ALIGNMENT, old_alignment); + glPixelStorei(GL_PACK_ROW_LENGTH, old_row_length); + return true; +} + void OpenGLHostDisplay::SetVSync(bool enabled) { // Window framebuffer has to be bound to call SetSwapInterval. diff --git a/src/duckstation-sdl/opengl_host_display.h b/src/duckstation-sdl/opengl_host_display.h index e54093f35..a10b6d170 100644 --- a/src/duckstation-sdl/opengl_host_display.h +++ b/src/duckstation-sdl/opengl_host_display.h @@ -3,8 +3,8 @@ #include "common/gl/texture.h" #include "core/host_display.h" #include -#include #include +#include class OpenGLHostDisplay final : public HostDisplay { @@ -26,6 +26,8 @@ public: bool dynamic) override; void UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, const void* data, u32 data_stride) override; + bool DownloadTexture(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, void* out_data, + u32 out_data_stride) override; void SetVSync(bool enabled) override;