From 36d18b209ef5480d93a605d41cd8fc9174f5d7ea Mon Sep 17 00:00:00 2001 From: Patoke Date: Wed, 4 Mar 2026 06:51:35 -0300 Subject: [PATCH] feat: game can now save and display thumbnails, code rebuilt from the xbox one edition of the game fix: Renderer::CaptureThumbnail now can capture thumbnails correctly fix: renderTargetViews and renderTargetShaderResourceViews are no longer null fix: texture saving functions were using BGRA instead of RGBA format --- Windows_Libs/Dev/Render/Renderer.h | 4 +- Windows_Libs/Dev/Render/RendererCore.cpp | 95 ++++++---- Windows_Libs/Dev/Render/RendererTexture.cpp | 4 +- Windows_Libs/Dev/Storage/STO_SaveGame.cpp | 183 +++++++++++++++++++- Windows_Libs/Dev/Storage/STO_SaveGame.h | 13 ++ 5 files changed, 262 insertions(+), 37 deletions(-) diff --git a/Windows_Libs/Dev/Render/Renderer.h b/Windows_Libs/Dev/Render/Renderer.h index 64887c1..346e05c 100644 --- a/Windows_Libs/Dev/Render/Renderer.h +++ b/Windows_Libs/Dev/Render/Renderer.h @@ -470,7 +470,9 @@ public: std::unordered_map managedRasterizerStates; bool m_bShouldScreenGrabNextFrame; bool m_bSuspended; - BYTE paddingAfterSuspendState[2]; + + // @Patoke add + ID3D11Texture2D *m_backBufferTexture = NULL; }; // Singleton diff --git a/Windows_Libs/Dev/Render/RendererCore.cpp b/Windows_Libs/Dev/Render/RendererCore.cpp index 58eef52..b0ce63b 100644 --- a/Windows_Libs/Dev/Render/RendererCore.cpp +++ b/Windows_Libs/Dev/Render/RendererCore.cpp @@ -263,6 +263,14 @@ void Renderer::CaptureThumbnail(ImageFileBuffer *pngOut) { Renderer::Context &c = getContext(); + // @Patoke fix: bind render target to a proper backbuffer texture + ID3D11Resource *actualBackBuffer = NULL; + renderTargetView->GetResource(&actualBackBuffer); + + // copy the backbuffer contents + c.m_pDeviceContext->CopyResource(m_backBufferTexture, actualBackBuffer); + actualBackBuffer->Release(); + float left; float bottom; float right; @@ -331,25 +339,25 @@ void Renderer::CaptureThumbnail(ImageFileBuffer *pngOut) float aspectRatio = IsWidescreen() ? (16.0f / 9.0f) : (4.0f / 3.0f); right *= aspectRatio; - left *= aspectRatio; + left *= aspectRatio; - float width = right - left; + float width = right - left; float height = top - bottom; if (height > width) { float diff = (height - width) * 0.5f; bottom += diff; - top -= diff; + top -= diff; } else { float diff = (width - height) * 0.5f; - left += diff; + left += diff; right -= diff; } - left /= aspectRatio; + left /= aspectRatio; right /= aspectRatio; ID3D11BlendState *blendState = NULL; @@ -410,7 +418,10 @@ void Renderer::CaptureThumbnail(ImageFileBuffer *pngOut) blendState->Release(); depthState->Release(); rasterizerState->Release(); - c.m_pDeviceContext->PSSetShaderResources(0, 0, NULL); + + // @Patoke add: just to make sure, set the render target shader resource to null to avoid any potential read/write hazards + ID3D11ShaderResourceView *nullSRV[1] = {nullptr}; + c.m_pDeviceContext->PSSetShaderResources(0, 1, nullSRV); for (UINT i = 0; i < MAX_MIP_LEVELS - 1; ++i) { @@ -422,6 +433,8 @@ void Renderer::CaptureThumbnail(ImageFileBuffer *pngOut) viewport.MinDepth = 0.0f; viewport.MaxDepth = 1.0f; + c.m_pDeviceContext->PSSetShaderResources(0, 1, nullSRV); + c.m_pDeviceContext->OMSetRenderTargets(1, &renderTargetViews[i], NULL); c.m_pDeviceContext->RSSetViewports(1, &viewport); @@ -433,23 +446,31 @@ void Renderer::CaptureThumbnail(ImageFileBuffer *pngOut) D3D11_MAPPED_SUBRESOURCE mapped = {}; c.m_pDeviceContext->Map(c.m_thumbnailBoundsBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped); - float* constants = static_cast(mapped.pData); + float *constants = static_cast(mapped.pData); + // @Patoke fix: the shader code zooms by 2x every iteration, so we keep zooming in over and over again if we use 1.0f, so we adjust by + // doing 2.0f like a boss if (i == 0) { constants[0] = left; constants[1] = bottom; - constants[2] = right - left; - constants[3] = top - bottom; + constants[2] = (right - left) * 2.0f; + constants[3] = (top - bottom) * 2.0f; } else { constants[0] = 0.0f; constants[1] = 0.0f; - constants[2] = 1.0f; - constants[3] = 1.0f; + constants[2] = 2.0f; + constants[3] = 2.0f; } c.m_pDeviceContext->Unmap(c.m_thumbnailBoundsBuffer, 0); + + // @Patoke fix: the shader expects the bounds buffer to be at slot 9 for vertex shader and slot 0 for pixel shader, so we need to set it there + // instead of the usual slot 0 + c.m_pDeviceContext->VSSetConstantBuffers(9, 1, &c.m_thumbnailBoundsBuffer); + c.m_pDeviceContext->PSSetConstantBuffers(0, 1, &c.m_thumbnailBoundsBuffer); + c.m_pDeviceContext->Draw(4, 0); } @@ -469,26 +490,21 @@ void Renderer::CaptureThumbnail(ImageFileBuffer *pngOut) c.m_pDeviceContext->CopyResource(stagingTexture, renderTargetTextures[MAX_MIP_LEVELS - 2]); D3D11_MAPPED_SUBRESOURCE mapped = {}; - c.m_pDeviceContext->Map(stagingTexture, 0, D3D11_MAP_READ_WRITE, 0, &mapped); - const unsigned char* src = static_cast(mapped.pData); - unsigned char* dst = linearData; - - for (UINT y = 0; y < kThumbnailSize; ++y) + if (SUCCEEDED(c.m_pDeviceContext->Map(stagingTexture, 0, D3D11_MAP_READ, 0, &mapped))) { - std::memcpy(dst, src, stride); - - unsigned char* alpha = dst + 3; - for (UINT x = 0; x < kThumbnailSize; ++x) + for (UINT y = 0; y < kThumbnailSize; ++y) { - *alpha = 0xFF; - alpha += 4; + unsigned char *dstRow = linearData + (y * stride); + const unsigned char *srcRow = reinterpret_cast(mapped.pData) + (y * mapped.RowPitch); + + std::memcpy(dstRow, srcRow, stride); + for (UINT x = 0; x < kThumbnailSize; ++x) + { + dstRow[(x * 4) + 3] = 0xFF; + } } - - src += mapped.RowPitch; - dst += stride; + c.m_pDeviceContext->Unmap(stagingTexture, 0); } - - c.m_pDeviceContext->Unmap(stagingTexture, 0); } ConvertLinearToPng(pngOut, linearData, kThumbnailSize, kThumbnailSize); @@ -708,10 +724,24 @@ void Renderer::Initialise(ID3D11Device *pDevice, IDXGISwapChain *pSwapChain) backBufferTexture->GetDesc(&backDesc); backBufferWidth = backDesc.Width; backBufferHeight = backDesc.Height; - renderTargetTextures[0] = backBufferTexture; - m_pDevice->CreateRenderTargetView(backBufferTexture, &rtvDesc, &renderTargetView); - m_pDevice->CreateShaderResourceView(backBufferTexture, &srvDesc, &renderTargetShaderResourceView); + // @Patoke fix: we can't bind the backbuffer directly as a shader resource, so we create a new texture with the same dimensions and format and + // copy the backbuffer contents to it, then we can bind that texture as a shader resource for the thumbnail generation + D3D11_TEXTURE2D_DESC safeDesc = backDesc; + safeDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + safeDesc.Usage = D3D11_USAGE_DEFAULT; + safeDesc.SampleDesc.Count = 1; // no MSAA + safeDesc.SampleDesc.Quality = 0; + + m_pDevice->CreateTexture2D(&safeDesc, NULL, &m_backBufferTexture); + + m_pDevice->CreateShaderResourceView(m_backBufferTexture, NULL, &renderTargetShaderResourceView); + + renderTargetTextures[0] = m_backBufferTexture; + + //m_pDevice->CreateRenderTargetView(backBufferTexture, &rtvDesc, &renderTargetView); + + backBufferTexture->Release(); backBufferResource->Release(); D3D11_TEXTURE2D_DESC desc = {}; @@ -731,9 +761,10 @@ void Renderer::Initialise(ID3D11Device *pDevice, IDXGISwapChain *pSwapChain) desc.Width = s_auiWidths[i + 1]; desc.Height = s_auiHeights[i + 1]; + // @Patoke fix: before these would fail and our views would be nullptrs m_pDevice->CreateTexture2D(&desc, NULL, &renderTargetTextures[i]); - m_pDevice->CreateRenderTargetView(renderTargetTextures[i], &rtvDesc, &renderTargetViews[i]); - m_pDevice->CreateShaderResourceView(renderTargetTextures[i], &srvDesc, &renderTargetShaderResourceViews[i]); + m_pDevice->CreateRenderTargetView(renderTargetTextures[i], NULL, &renderTargetViews[i]); + m_pDevice->CreateShaderResourceView(renderTargetTextures[i], NULL, &renderTargetShaderResourceViews[i]); } std::memset(m_textures, 0, sizeof(m_textures)); diff --git a/Windows_Libs/Dev/Render/RendererTexture.cpp b/Windows_Libs/Dev/Render/RendererTexture.cpp index 0ffeb10..f666f1f 100644 --- a/Windows_Libs/Dev/Render/RendererTexture.cpp +++ b/Windows_Libs/Dev/Render/RendererTexture.cpp @@ -95,7 +95,7 @@ HRESULT Renderer::SaveTextureData(const char* szFilename, D3DXIMAGE_INFO* pSrcIn image.width = pSrcInfo->Width; image.height = pSrcInfo->Height; image.version = PNG_IMAGE_VERSION; - image.format = PNG_FORMAT_BGRA; + image.format = PNG_FORMAT_RGBA; png_image_write_to_file(&image, szFilename, NULL, ppDataOut, NULL, NULL); return S_OK; @@ -111,7 +111,7 @@ HRESULT Renderer::SaveTextureDataToMemory(void* pOutput, int outputCapacity, int image.height = height; dataEnd = (BYTE *)pOutput + outputCapacity; image.version = PNG_IMAGE_VERSION; - image.format = PNG_FORMAT_BGRA; + image.format = PNG_FORMAT_RGBA; dataStart = (BYTE*)pOutput; dataCurr = (BYTE*)pOutput; diff --git a/Windows_Libs/Dev/Storage/STO_SaveGame.cpp b/Windows_Libs/Dev/Storage/STO_SaveGame.cpp index fcd92d1..9250219 100644 --- a/Windows_Libs/Dev/Storage/STO_SaveGame.cpp +++ b/Windows_Libs/Dev/Storage/STO_SaveGame.cpp @@ -23,6 +23,10 @@ SOFTWARE. */ #include "STO_SaveGame.h" +#include "STO_Main.h" + +// @Patoke add +char CSaveGame::szPNGHeader[] = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"; CSaveGame::CSaveGame() { @@ -43,6 +47,12 @@ CSaveGame::CSaveGame() GetCurrentDirectoryA(sizeof(dirName), dirName); sprintf(curDir, "%s/Windows64/GameHDD/", dirName); CreateDirectoryA(curDir, 0); + + // @Patoke add + this->m_pbThumbnailData = nullptr; + this->m_uiThumbnailSize = 0; + this->m_pbImageData = nullptr; + this->m_uiImageSize = 0; } void CSaveGame::SetSaveDisabled(bool bDisable) @@ -124,7 +134,14 @@ C4JStorage::ESaveGameState CSaveGame::GetSavesInfo(int iPad, int (*Func)(LPVOID sprintf(fileName, "%s\\Windows64\\GameHDD\\%s\\saveData.ms", dirName, findFileData.cFileName); GetFileAttributesExA(fileName, GetFileExInfoStandard, &fileInfoBuffer); - m_pSaveDetails->SaveInfoA[i++].metaData.dataSize = fileInfoBuffer.nFileSizeLow; + m_pSaveDetails->SaveInfoA[i].metaData.dataSize = fileInfoBuffer.nFileSizeLow; + + char thumbName[280]; + sprintf(thumbName, "%s\\Windows64\\GameHDD\\%s\\thumbData.png", dirName, findFileData.cFileName); + + GetFileAttributesExA(thumbName, GetFileExInfoStandard, &fileInfoBuffer); + m_pSaveDetails->SaveInfoA[i++].metaData.thumbnailSize = fileInfoBuffer.nFileSizeLow; + m_pSaveDetails->iSaveC++; } } while (FindNextFileA(fi, &findFileData)); @@ -165,9 +182,46 @@ void CSaveGame::ClearSavesInfo() } } +// @Patoke add C4JStorage::ESaveGameState CSaveGame::LoadSaveDataThumbnail(PSAVE_INFO pSaveInfo, int (*Func)(LPVOID lpParam, PBYTE pbThumbnail, DWORD dwThumbnailBytes), LPVOID lpParam) { + if (pSaveInfo == nullptr) + { + return C4JStorage::ESaveGame_Idle; + } + + DWORD thumbSize = pSaveInfo->metaData.thumbnailSize; + if (thumbSize > 0 && pSaveInfo->thumbnailData == nullptr) + { + char curDir[256]; + GetCurrentDirectoryA(sizeof(curDir), curDir); + + const char *saveName = (const char *)pSaveInfo; + + char thumbPath[512]; + sprintf(thumbPath, "%s/Windows64/GameHDD/%s/thumbData.ms", curDir, saveName); + + HANDLE hThumb = CreateFileA(thumbPath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); + + if (hThumb != INVALID_HANDLE_VALUE) + { + pSaveInfo->thumbnailData = new BYTE[thumbSize]; + + DWORD bytesRead = 0; + BOOL res = ReadFile(hThumb, pSaveInfo->thumbnailData, thumbSize, &bytesRead, 0); + + // If the read fails or is incomplete, clean up to prevent corrupted image data + if (!res || bytesRead != thumbSize) + { + delete[] pSaveInfo->thumbnailData; + pSaveInfo->thumbnailData = nullptr; + } + + CloseHandle(hThumb); + } + } + Func(lpParam, pSaveInfo->thumbnailData, pSaveInfo->metaData.thumbnailSize); return C4JStorage::ESaveGame_GetSaveThumbnail; } @@ -259,9 +313,118 @@ PVOID CSaveGame::AllocateSaveData(unsigned int uiBytes) return m_pSaveData; } +// @Patoke add void CSaveGame::SetSaveImages(PBYTE pbThumbnail, DWORD dwThumbnailBytes, PBYTE pbImage, DWORD dwImageBytes, PBYTE pbTextData, DWORD dwTextDataBytes) { - ; + if (this->m_pbThumbnailData) + { + free(this->m_pbThumbnailData); + } + + this->m_pbImageData = pbImage; + this->m_uiImageSize = dwImageBytes; + + DWORD dwNewThumbnailBytes = dwThumbnailBytes; + if (dwTextDataBytes > 0) + { + // add extra bytes to the allocation for the text chunk (4 bytes for size, 4 bytes for type, 4 bytes for checksum) + dwNewThumbnailBytes += dwTextDataBytes + 12; + } + + // allocate the thumbnail + this->m_pbThumbnailData = static_cast(malloc(dwNewThumbnailBytes)); + this->m_uiThumbnailSize = dwNewThumbnailBytes; + memset(this->m_pbThumbnailData, 0, dwNewThumbnailBytes); + + // copy original thumbnail data to new buffer + memcpy(this->m_pbThumbnailData, pbThumbnail, dwThumbnailBytes); + + // inject text metadata into the thumbnail if it exists + if (dwTextDataBytes > 0) + { + this->AddTextFieldToPNG(reinterpret_cast(this->m_pbThumbnailData), dwThumbnailBytes, pbTextData, dwTextDataBytes, + dwNewThumbnailBytes); + } +} + +// @Patoke add +void CSaveGame::AddTextFieldToPNG(PBYTE pbImageData, DWORD dwImageBytes, PBYTE pbTextData, DWORD dwTextBytes, DWORD dwTotalSizeAllocated) +{ + if (dwImageBytes == 0) + { + return; + } + + for (int j = 0; j < 8; ++j) + { + if (CSaveGame::szPNGHeader[j] != pbImageData[j]) + { + return; + } + } + + unsigned int offset = 8; + while (offset < dwImageBytes) + { + unsigned int chunkStart = offset; + + unsigned int chunkLength = this->ReverseBytes(*reinterpret_cast(&pbImageData[offset])); + offset += 4; + + unsigned int chunkType = this->ReverseBytes(*reinterpret_cast(&pbImageData[offset])); + offset += 4; + + if (chunkType == 'IEND') + { + offset = chunkStart; + + // write the tEXt chunk before the IEND chunk + *reinterpret_cast(&pbImageData[offset]) = this->ReverseBytes(static_cast(dwTextBytes)); + offset += 4; + + unsigned __int8 *textTypeStart = &pbImageData[offset]; + *reinterpret_cast(&pbImageData[offset]) = this->ReverseBytes('tEXt'); + offset += 4; + + memcpy(&pbImageData[offset], pbTextData, dwTextBytes); + offset += dwTextBytes; + + unsigned int textCrc = InternalStorageManager.CRC(textTypeStart, dwTextBytes + 4); + *reinterpret_cast(&pbImageData[offset]) = this->ReverseBytes(textCrc); + offset += 4; + + // add a new IEND chunk + *reinterpret_cast(&pbImageData[offset]) = 0; + offset += 4; + + unsigned __int8 *iendTypeStart = &pbImageData[offset]; + *reinterpret_cast(&pbImageData[offset]) = this->ReverseBytes('IEND'); + offset += 4; + + unsigned int iendCrc = InternalStorageManager.CRC(iendTypeStart, 4); + *reinterpret_cast(&pbImageData[offset]) = this->ReverseBytes(iendCrc); + offset += 4; + + assert("uiCount <= dwTotalSizeAllocated"); + + break; + } + else + { + // not 'IEND' chunk, continue to next chunk + offset += chunkLength + 4; + } + } +} + +// @Patoke add +unsigned int CSaveGame::ReverseBytes(unsigned int uiValue) +{ + unsigned int uiReturn = 0; + + uiReturn = (uiValue << 24) | ((uiValue << 8) & 0x00FF0000) | ((uiValue >> 8) & 0x0000FF00) | (uiValue >> 24); + + return uiReturn; } C4JStorage::ESaveGameState CSaveGame::SaveSaveData(int (*Func)(LPVOID, const bool), LPVOID lpParam) @@ -269,6 +432,8 @@ C4JStorage::ESaveGameState CSaveGame::SaveSaveData(int (*Func)(LPVOID, const boo char dirName[256]; char curDir[256]; char fileName[280]; + char thumbName[280]; + GetCurrentDirectoryA(sizeof(curDir), curDir); sprintf(dirName, "%s/Windows64/GameHDD/%s", curDir, m_szSaveUniqueName); CreateDirectoryA(dirName, 0); @@ -282,6 +447,20 @@ C4JStorage::ESaveGameState CSaveGame::SaveSaveData(int (*Func)(LPVOID, const boo CloseHandle(h); + // @Patoke add + if (this->m_pbThumbnailData != nullptr && this->m_uiThumbnailSize > 0) + { + sprintf(thumbName, "%s/thumbData.png", dirName); + + HANDLE hThumb = CreateFileA(thumbName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); + + DWORD thumbBytesWritten = 0; + BOOL thumbRes = WriteFile(hThumb, this->m_pbThumbnailData, this->m_uiThumbnailSize, &thumbBytesWritten, 0); + _ASSERT(thumbRes && thumbBytesWritten == this->m_uiThumbnailSize); + + CloseHandle(hThumb); + } + Func(lpParam, true); return C4JStorage::ESaveGame_Idle; diff --git a/Windows_Libs/Dev/Storage/STO_SaveGame.h b/Windows_Libs/Dev/Storage/STO_SaveGame.h index 782e022..07b1940 100644 --- a/Windows_Libs/Dev/Storage/STO_SaveGame.h +++ b/Windows_Libs/Dev/Storage/STO_SaveGame.h @@ -53,6 +53,10 @@ public: void CreateSaveUniqueName(void); + // @Patoke add: definition taken from the Xbox One binaries + void AddTextFieldToPNG(PBYTE pbImageData, DWORD dwImageBytes, PBYTE pbTextData, DWORD dwTextBytes, DWORD dwTotalSizeAllocated); + unsigned int ReverseBytes(unsigned int uiValue); + void *m_pSaveData; unsigned int m_uiSaveSize; char m_szSaveUniqueName[32]; @@ -60,4 +64,13 @@ public: bool m_bIsSafeDisabled; bool m_bHasSaveDetails; SAVE_DETAILS *m_pSaveDetails; + + // @Patoke add + PBYTE m_pbThumbnailData; + unsigned int m_uiThumbnailSize; + + PBYTE m_pbImageData; + unsigned int m_uiImageSize; + + static char szPNGHeader[]; }; \ No newline at end of file