From c25dbdf86dce78d489aa087b2fcb20765de6b2a7 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:24:21 +0200 Subject: [PATCH] Latte: Partial support for mips in texture readback --- src/Cafe/HW/Latte/Core/LatteTexture.h | 2 +- src/Cafe/HW/Latte/Core/LatteTextureLoader.cpp | 28 ++++++++---------- .../HW/Latte/Core/LatteTextureReadback.cpp | 4 +-- .../HW/Latte/Core/LatteTextureReadbackInfo.h | 8 +++-- .../Metal/LatteTextureReadbackMtl.cpp | 1 + .../Latte/Renderer/OpenGL/OpenGLRenderer.cpp | 1 - .../Renderer/OpenGL/TextureReadbackGL.cpp | 29 ++++++++++--------- .../Renderer/Vulkan/TextureReadbackVk.cpp | 1 + 8 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/Cafe/HW/Latte/Core/LatteTexture.h b/src/Cafe/HW/Latte/Core/LatteTexture.h index 6c09e840..8b08197f 100644 --- a/src/Cafe/HW/Latte/Core/LatteTexture.h +++ b/src/Cafe/HW/Latte/Core/LatteTexture.h @@ -333,7 +333,7 @@ float* LatteTexture_getEffectiveTextureScale(LatteConst::ShaderType shaderType, LatteTextureView* LatteTexture_CreateTexture(Latte::E_DIM dim, MPTR physAddress, MPTR physMipAddress, Latte::E_GX2SURFFMT format, uint32 width, uint32 height, uint32 depth, uint32 pitch, uint32 mipLevels, uint32 swizzle, Latte::E_HWTILEMODE tileMode, bool isDepth); void LatteTexture_Delete(LatteTexture* texture); -void LatteTextureLoader_writeReadbackTextureToMemory(LatteTextureDefinition* textureData, uint32 sliceIndex, uint32 mipIndex, uint8* linearPixelData); +void LatteTextureLoader_writeReadbackTextureToMemory(LatteTextureDefinition* textureData, uint32 sliceIndex, uint32 mipIndex, uint8* linearPixelData, uint32 sourceRowPitch); sint32 LatteTexture_getEffectiveWidth(LatteTexture* texture); bool LatteTexture_doesEffectiveRescaleRatioMatch(LatteTexture* texture1, sint32 mipLevel1, LatteTexture* texture2, sint32 mipLevel2); diff --git a/src/Cafe/HW/Latte/Core/LatteTextureLoader.cpp b/src/Cafe/HW/Latte/Core/LatteTextureLoader.cpp index 7e196881..faff2d52 100644 --- a/src/Cafe/HW/Latte/Core/LatteTextureLoader.cpp +++ b/src/Cafe/HW/Latte/Core/LatteTextureLoader.cpp @@ -695,15 +695,12 @@ void LatteTextureLoader_UpdateTextureSliceData(LatteTexture* tex, uint32 sliceIn } template -void optimizedLinearReadbackWriteLoop(LatteTextureLoaderCtx* textureLoader, uint8* linearPixelData) +void optimizedLinearReadbackWriteLoop(LatteTextureLoaderCtx* textureLoader, uint8* linearPixelData, uint32 sourcePitch) { - uint32 pitch = textureLoader->width; // optimized for linear for (sint32 y = 0; y < textureLoader->height; y++) { - sint32 yc = y; - sint32 pixelOffset = yc * pitch; - copyType* rowPixelData = (copyType*)(linearPixelData + pixelOffset * sizeof(copyType)); + copyType* rowPixelData = (copyType*)(linearPixelData + y * sourcePitch); copyType* blockData = (copyType*)LatteTextureLoader_getInputLinearOptimized_(textureLoader, 0, y, 1, 1, sizeof(copyType) * 8, textureLoader->sliceIndex, 1, 0, textureLoader->pitch, textureLoader->surfaceInfoHeight); if constexpr (sizeof(copyType) == 4) { @@ -721,7 +718,7 @@ void optimizedLinearReadbackWriteLoop(LatteTextureLoaderCtx* textureLoader, uint } } -void LatteTextureLoader_writeReadbackTextureToMemory(LatteTextureDefinition* textureData, uint32 sliceIndex, uint32 mipIndex, uint8* linearPixelData) +void LatteTextureLoader_writeReadbackTextureToMemory(LatteTextureDefinition* textureData, uint32 sliceIndex, uint32 mipIndex, uint8* linearPixelData, uint32 sourceRowPitch) { LatteTextureLoaderCtx textureLoader = { 0 }; LatteTextureLoader_begin(&textureLoader, sliceIndex, mipIndex, textureData->physAddress, textureData->physMipAddress, textureData->format, textureData->dim, textureData->width, textureData->height, textureData->depth, textureData->mipLevels, textureData->pitch, textureData->tileMode, textureData->swizzle); @@ -736,22 +733,21 @@ void LatteTextureLoader_writeReadbackTextureToMemory(LatteTextureDefinition* tex if (textureData->tileMode == Latte::E_HWTILEMODE::TM_LINEAR_ALIGNED) { - uint32 pitch = textureLoader.width; if (textureData->format == Latte::E_GX2SURFFMT::R8_G8_B8_A8_UNORM || textureData->format == Latte::E_GX2SURFFMT::R8_G8_B8_A8_SRGB) { - optimizedLinearReadbackWriteLoop(&textureLoader, linearPixelData); + optimizedLinearReadbackWriteLoop(&textureLoader, linearPixelData, sourceRowPitch); } else if (textureData->format == Latte::E_GX2SURFFMT::R16_G16_B16_A16_UNORM) { - optimizedLinearReadbackWriteLoop(&textureLoader, linearPixelData); + optimizedLinearReadbackWriteLoop(&textureLoader, linearPixelData, sourceRowPitch); } else if (textureData->format == Latte::E_GX2SURFFMT::R32_G32_B32_A32_FLOAT) { for (sint32 y = 0; y < textureLoader.height; y += textureLoader.stepY) { sint32 yc = y; - sint32 pixelOffset = (0 + yc * pitch) * 16; + sint32 pixelOffset = yc * sourceRowPitch; for (sint32 x = 0; x < textureLoader.width; x += textureLoader.stepX) { uint8* blockData = LatteTextureLoader_getInputLinearOptimized(&textureLoader, x, y); @@ -771,7 +767,7 @@ void LatteTextureLoader_writeReadbackTextureToMemory(LatteTextureDefinition* tex for (sint32 x = 0; x < textureLoader.width; x += textureLoader.stepX) { uint8* blockData = LatteTextureLoader_getInputLinearOptimized(&textureLoader, x, y); - sint32 pixelOffset = (x + yc * pitch) * 4; + sint32 pixelOffset = yc * sourceRowPitch + x * 4; (*(uint32*)(blockData + 0)) = *(uint32*)(linearPixelData + pixelOffset + 0); } } @@ -784,7 +780,7 @@ void LatteTextureLoader_writeReadbackTextureToMemory(LatteTextureDefinition* tex for (sint32 x = 0; x < textureLoader.width; x += textureLoader.stepX) { uint8* blockData = LatteTextureLoader_getInputLinearOptimized(&textureLoader, x, y); - sint32 pixelOffset = (x + yc * pitch) * 8; + sint32 pixelOffset = yc * sourceRowPitch + x * 8; (*(uint32*)(blockData + 0)) = *(uint32*)(linearPixelData + pixelOffset + 0); (*(uint32*)(blockData + 4)) = *(uint32*)(linearPixelData + pixelOffset + 4); } @@ -792,7 +788,7 @@ void LatteTextureLoader_writeReadbackTextureToMemory(LatteTextureDefinition* tex } else if (textureData->format == Latte::E_GX2SURFFMT::R8_G8_UNORM) { - optimizedLinearReadbackWriteLoop(&textureLoader, linearPixelData); + optimizedLinearReadbackWriteLoop(&textureLoader, linearPixelData, sourceRowPitch); } else if (textureData->format == Latte::E_GX2SURFFMT::R16_G16_B16_A16_UNORM) { @@ -800,7 +796,7 @@ void LatteTextureLoader_writeReadbackTextureToMemory(LatteTextureDefinition* tex } else if (textureData->format == Latte::E_GX2SURFFMT::R16_UNORM) { - optimizedLinearReadbackWriteLoop(&textureLoader, linearPixelData); + optimizedLinearReadbackWriteLoop(&textureLoader, linearPixelData, sourceRowPitch); } else { @@ -816,7 +812,7 @@ void LatteTextureLoader_writeReadbackTextureToMemory(LatteTextureDefinition* tex // used in Bayonetta 2 for (sint32 y = 0; y < textureLoader.height; y++) { - uint8* pixelInput = linearPixelData + (y * textureLoader.width) * 4; + uint8* pixelInput = linearPixelData + y * sourceRowPitch; for (sint32 x = 0; x < textureLoader.width; x++) { uint8* outputData = LatteTextureLoader_GetInput(&textureLoader, x, y); @@ -831,7 +827,7 @@ void LatteTextureLoader_writeReadbackTextureToMemory(LatteTextureDefinition* tex // Bayonetta 2 also uses this but it converts the depth buffer to a color texture first for (sint32 y = 0; y < textureLoader.height; y++) { - uint8* pixelInput = linearPixelData + (y * textureLoader.width) * 4; + uint8* pixelInput = linearPixelData + y * sourceRowPitch; for (sint32 x = 0; x < textureLoader.width; x++) { uint8* outputData = LatteTextureLoader_GetInput(&textureLoader, x, y); diff --git a/src/Cafe/HW/Latte/Core/LatteTextureReadback.cpp b/src/Cafe/HW/Latte/Core/LatteTextureReadback.cpp index db1b0a3b..5c46cc2e 100644 --- a/src/Cafe/HW/Latte/Core/LatteTextureReadback.cpp +++ b/src/Cafe/HW/Latte/Core/LatteTextureReadback.cpp @@ -145,7 +145,7 @@ void LatteTextureReadback_UpdateFinishedTransfers(bool forceFinish) } #endif uint8* pixelData = readbackInfo->GetData(); - LatteTextureLoader_writeReadbackTextureToMemory(&readbackInfo->hostTextureCopy, readbackInfo->m_firstSlice, 0, pixelData); + LatteTextureLoader_writeReadbackTextureToMemory(&readbackInfo->hostTextureCopy, readbackInfo->m_firstSlice, readbackInfo->m_firstMip, pixelData, readbackInfo->m_rowPitch); readbackInfo->ReleaseData(); // get the original texture if it still exists and invalidate the current data hash LatteTextureView* origTexView = LatteTextureViewLookupCache::lookupSlice(readbackInfo->hostTextureCopy.physAddress, readbackInfo->hostTextureCopy.width, readbackInfo->hostTextureCopy.height, readbackInfo->hostTextureCopy.pitch, 0, 0, readbackInfo->hostTextureCopy.format); @@ -172,7 +172,7 @@ bool LatteTextureReadback_ReadbackToLinearBlocking(LatteTextureView* sourceView, uint8* data = info->GetData(); // returned pixel format should match Latte format uint32 bpp = Latte::GetFormatBits(sourceView->baseTexture->format) / 8; - uint32 srcRowBytes = sourceView->baseTexture->width * bpp; + uint32 srcRowBytes = info->m_rowPitch; uint32 dstRowBytes = dstWidth * bpp; for (uint32 y = 0; y < dstHeight; y++) memcpy(dstPtr + y * dstPitch * bpp, data + y * srcRowBytes, dstRowBytes); diff --git a/src/Cafe/HW/Latte/Core/LatteTextureReadbackInfo.h b/src/Cafe/HW/Latte/Core/LatteTextureReadbackInfo.h index d076c840..4802c227 100644 --- a/src/Cafe/HW/Latte/Core/LatteTextureReadbackInfo.h +++ b/src/Cafe/HW/Latte/Core/LatteTextureReadbackInfo.h @@ -6,8 +6,8 @@ class LatteTextureReadbackInfo { public: - LatteTextureReadbackInfo(LatteTextureView* textureView) - : hostTextureCopy(textureView->baseTexture), m_firstSlice(textureView->firstSlice), m_textureView(textureView) + LatteTextureReadbackInfo(LatteTextureView* textureView, sint32 mipIndex = 0) + : hostTextureCopy(textureView->baseTexture), m_firstSlice(textureView->firstSlice), m_firstMip(mipIndex), m_textureView(textureView) {} virtual ~LatteTextureReadbackInfo() = default; @@ -25,8 +25,10 @@ public: // texture info LatteTextureDefinition hostTextureCopy{}; sint32 m_firstSlice{}; + sint32 m_firstMip{}; // mip level transferred by the backend + uint32 m_rowPitch = 0; // bytes between rows in GetData(). Set by the backend protected: LatteTextureView* m_textureView; uint32 m_image_size = 0; -}; \ No newline at end of file +}; diff --git a/src/Cafe/HW/Latte/Renderer/Metal/LatteTextureReadbackMtl.cpp b/src/Cafe/HW/Latte/Renderer/Metal/LatteTextureReadbackMtl.cpp index 683cfe34..2f31f6b2 100644 --- a/src/Cafe/HW/Latte/Renderer/Metal/LatteTextureReadbackMtl.cpp +++ b/src/Cafe/HW/Latte/Renderer/Metal/LatteTextureReadbackMtl.cpp @@ -19,6 +19,7 @@ void LatteTextureReadbackInfoMtl::StartTransfer() cemu_assert_debug(m_textureView->baseTexture->dim != Latte::E_DIM::DIM_3D); size_t bytesPerRow = GetMtlTextureBytesPerRow(baseTexture->format, baseTexture->isDepth, baseTexture->width); + m_rowPitch = static_cast(bytesPerRow); size_t bytesPerImage = GetMtlTextureBytesPerImage(baseTexture->format, baseTexture->isDepth, baseTexture->height, bytesPerRow); auto blitCommandEncoder = m_mtlr->GetBlitCommandEncoder(); diff --git a/src/Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.cpp b/src/Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.cpp index 9d30da31..e347b760 100644 --- a/src/Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.cpp @@ -542,7 +542,6 @@ void OpenGLRenderer::HandleScreenshotRequest(LatteTextureView* texView, bool pad texture_bindAndActivate(texView, 0); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &screenshotWidth); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &screenshotHeight); - glPixelStorei(GL_PACK_ALIGNMENT, 1); // set alignment to 1 const sint32 pixelDataSize = screenshotWidth * screenshotHeight * 3; std::vector rgb_data(pixelDataSize); diff --git a/src/Cafe/HW/Latte/Renderer/OpenGL/TextureReadbackGL.cpp b/src/Cafe/HW/Latte/Renderer/OpenGL/TextureReadbackGL.cpp index f978a598..d68453e0 100644 --- a/src/Cafe/HW/Latte/Renderer/OpenGL/TextureReadbackGL.cpp +++ b/src/Cafe/HW/Latte/Renderer/OpenGL/TextureReadbackGL.cpp @@ -2,25 +2,26 @@ #include "Cafe/HW/Latte/Renderer/OpenGL/LatteTextureGL.h" LatteTextureReadbackInfoGL::LatteTextureReadbackInfoGL(LatteTextureView* textureView) - : LatteTextureReadbackInfo(textureView) + : LatteTextureReadbackInfo(textureView, textureView->firstMip) { LatteTexture* baseTexture = textureView->baseTexture; + uint32 width = baseTexture->GetMipWidth(m_firstMip); // handle format if (textureView->format == Latte::E_GX2SURFFMT::R8_G8_B8_A8_UNORM) { - m_image_size = baseTexture->width*baseTexture->height * 4; + m_rowPitch = width * 4; m_texFormatGL = GL_RGBA; m_texDataTypeGL = GL_UNSIGNED_BYTE; } else if (textureView->format == Latte::E_GX2SURFFMT::R8_G8_B8_A8_SRGB) { - m_image_size = baseTexture->width*baseTexture->height * 4; + m_rowPitch = width * 4; m_texFormatGL = GL_RGBA; m_texDataTypeGL = GL_UNSIGNED_BYTE; } else if (textureView->format == Latte::E_GX2SURFFMT::R32_G32_B32_A32_FLOAT) { - m_image_size = baseTexture->width*baseTexture->height * 16; + m_rowPitch = width * 16; m_texFormatGL = GL_RGBA; m_texDataTypeGL = GL_FLOAT; } @@ -28,13 +29,13 @@ LatteTextureReadbackInfoGL::LatteTextureReadbackInfoGL(LatteTextureView* texture { if (baseTexture->isDepth) { - m_image_size = baseTexture->width*baseTexture->height * 4; + m_rowPitch = width * 4; m_texFormatGL = GL_DEPTH_COMPONENT; m_texDataTypeGL = GL_FLOAT; } else { - m_image_size = baseTexture->width*baseTexture->height * 4; + m_rowPitch = width * 4; m_texFormatGL = GL_RED; m_texDataTypeGL = GL_FLOAT; } @@ -43,33 +44,33 @@ LatteTextureReadbackInfoGL::LatteTextureReadbackInfoGL(LatteTextureView* texture { if (baseTexture->isDepth) { - m_image_size = baseTexture->width*baseTexture->height * 2; + m_rowPitch = width * 2; m_texFormatGL = GL_DEPTH_COMPONENT; m_texDataTypeGL = GL_UNSIGNED_SHORT; cemu_assert_unimplemented(); } else { - m_image_size = baseTexture->width*baseTexture->height * 2; + m_rowPitch = width * 2; m_texFormatGL = GL_RED; m_texDataTypeGL = GL_UNSIGNED_SHORT; } } else if (textureView->format == Latte::E_GX2SURFFMT::R16_G16_B16_A16_FLOAT) { - m_image_size = baseTexture->width*baseTexture->height * 8; + m_rowPitch = width * 8; m_texFormatGL = GL_RGBA; m_texDataTypeGL = GL_HALF_FLOAT; } else if (textureView->format == Latte::E_GX2SURFFMT::R8_G8_UNORM) { - m_image_size = baseTexture->width*baseTexture->height * 2; + m_rowPitch = width * 2; m_texFormatGL = GL_RG; m_texDataTypeGL = GL_UNSIGNED_BYTE; } else if (textureView->format == Latte::E_GX2SURFFMT::R16_G16_B16_A16_UNORM) { - m_image_size = baseTexture->width*baseTexture->height * 8; + m_rowPitch = width * 8; m_texFormatGL = GL_RGBA; m_texDataTypeGL = GL_UNSIGNED_SHORT; } @@ -78,6 +79,9 @@ LatteTextureReadbackInfoGL::LatteTextureReadbackInfoGL(LatteTextureView* texture cemuLog_logDebug(LogType::Force, "Unsupported texture readback format {:04x}", (uint32)textureView->format); return; } + // OpenGL uses the default 4 byte pack alignment because it gives us slightly better row copy performance + m_rowPitch = (m_rowPitch + 3) & ~3u; + m_image_size = m_rowPitch * baseTexture->GetMipHeight(m_firstMip); } LatteTextureReadbackInfoGL::~LatteTextureReadbackInfoGL() @@ -93,14 +97,13 @@ void LatteTextureReadbackInfoGL::StartTransfer() { cemu_assert(m_textureView); LatteTextureGL* baseTexture = (LatteTextureGL*)m_textureView->baseTexture; - cemu_assert_debug(m_textureView->firstMip == 0); cemu_assert_debug(m_textureView->baseTexture->dim != Latte::E_DIM::DIM_3D); // create unsynchronized buffer glGenBuffers(1, &texImageBufferGL); glBindBuffer(GL_PIXEL_PACK_BUFFER, texImageBufferGL); glBufferData(GL_PIXEL_PACK_BUFFER, m_image_size, NULL, GL_DYNAMIC_READ); // request texture read into buffer - glGetTextureSubImage(baseTexture->glId_texture, 0, 0, 0, m_firstSlice, baseTexture->width, baseTexture->height, 1, m_texFormatGL, m_texDataTypeGL, m_image_size, NULL); + glGetTextureSubImage(baseTexture->glId_texture, m_firstMip, 0, 0, m_firstSlice, baseTexture->GetMipWidth(m_firstMip), baseTexture->GetMipHeight(m_firstMip), 1, m_texFormatGL, m_texDataTypeGL, m_image_size, NULL); glFlush(); // create fence sync (so we can check if the image copy operation finished) imageCopyFinSync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/TextureReadbackVk.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/TextureReadbackVk.cpp index a4556c45..33fa3d6c 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/TextureReadbackVk.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/TextureReadbackVk.cpp @@ -6,6 +6,7 @@ LatteTextureReadbackInfoVk::LatteTextureReadbackInfoVk(VkDevice device, LatteTex : LatteTextureReadbackInfo(textureView), m_device(device) { m_image_size = GetImageSize(textureView); + m_rowPitch = m_image_size / textureView->baseTexture->height; } LatteTextureReadbackInfoVk::~LatteTextureReadbackInfoVk()