diff --git a/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp index 5945e10e5..bf269344f 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp @@ -110,6 +110,8 @@ UIScene_LoadMenu::UIScene_LoadMenu(int iPad, void *initData, UILayer *parentLaye m_bThumbnailGetFailed = false; m_seed = 0; m_bIsCorrupt = false; + m_pbThumbnailData = nullptr; + m_uiThumbnailSize = 0; m_bMultiplayerAllowed = ProfileManager.IsSignedInLive( m_iPad ) && ProfileManager.AllowedToPlayMultiplayer(m_iPad); // 4J-PB - read the settings for the online flag. We'll only save this setting if the user changed it. @@ -249,13 +251,73 @@ UIScene_LoadMenu::UIScene_LoadMenu(int iPad, void *initData, UILayer *parentLaye #endif #endif #ifdef _WINDOWS64 - if (params->saveDetails != nullptr && params->saveDetails->UTF8SaveName[0] != '\0') + if (params->saveDetails != nullptr) { - wchar_t wSaveName[128]; - ZeroMemory(wSaveName, sizeof(wSaveName)); - mbstowcs(wSaveName, params->saveDetails->UTF8SaveName, 127); - m_levelName = wstring(wSaveName); - m_labelGameName.init(m_levelName); + if (params->saveDetails->UTF8SaveName[0] != '\0') + { + wchar_t wSaveName[128]; + ZeroMemory(wSaveName, sizeof(wSaveName)); + mbstowcs(wSaveName, params->saveDetails->UTF8SaveName, 127); + m_levelName = wstring(wSaveName); + m_labelGameName.init(m_levelName); + } + + // texture name = the save folder name in wide chars + wchar_t wFilename[MAX_SAVEFILENAME_LENGTH]; + ZeroMemory(wFilename, sizeof(wFilename)); + mbstowcs(wFilename, params->saveDetails->UTF8SaveFilename, MAX_SAVEFILENAME_LENGTH - 1); + m_thumbnailName = wFilename; + + // use whatever thumbnail the save list already loaded, or read it + // from disk. don't set m_bSaveThumbnailReady because tick() calls + // NavigateBack() to dismiss the timer scene which we never showed + PBYTE thumbData = nullptr; + DWORD thumbSize = 0; + + if (params->saveDetails->pbThumbnailData && params->saveDetails->dwThumbnailSize > 0) + { + thumbData = params->saveDetails->pbThumbnailData; + thumbSize = params->saveDetails->dwThumbnailSize; + } + else + { + // not in memory yet, try the file on disk + char thumbPath[MAX_PATH]; + sprintf_s(thumbPath, MAX_PATH, "Windows64\\GameHDD\\%s\\thumbnail.png", + params->saveDetails->UTF8SaveFilename); + + HANDLE hFile = CreateFileA(thumbPath, GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr); + if (hFile != INVALID_HANDLE_VALUE) + { + DWORD fileSize = GetFileSize(hFile, nullptr); + if (fileSize > 0 && fileSize != INVALID_FILE_SIZE && fileSize < 4 * 1024 * 1024) + { + PBYTE fileData = new BYTE[fileSize]; + DWORD bytesRead = 0; + if (ReadFile(hFile, fileData, fileSize, &bytesRead, nullptr) && bytesRead == fileSize) + { + thumbData = fileData; + thumbSize = fileSize; + app.DebugPrintf("LoadMenu: Loaded thumbnail.png from disk\n"); + } + else + { + delete[] fileData; + } + } + CloseHandle(hFile); + } + } + + if (thumbData && thumbSize > 0) + { + registerSubstitutionTexture(wFilename, thumbData, thumbSize); + m_bitmapIcon.setTextureName(wFilename); + m_pbThumbnailData = thumbData; + m_uiThumbnailSize = thumbSize; + } + m_bRetrievingSaveThumbnail = false; } #endif } diff --git a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp index 3810b3aa4..73ec5d170 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp @@ -822,6 +822,63 @@ void UIScene_LoadOrJoinMenu::tick() } } +#ifdef _WINDOWS64 + // LoadSaveDataThumbnail in the storage lib doesn't work on Win64, + // so just read thumbnail.png straight from each save folder + if(!m_bExitScene && m_bSavesDisplayed && !m_bAllLoaded) + { + PSAVE_DETAILS pSaveDetails = StorageManager.ReturnSavesInfo(); + int totalSaves = m_buttonListSaves.getItemCount() - m_iDefaultButtonsC; + + while (m_iRequestingThumbnailId < totalSaves) + { + int saveId = m_saveDetails[m_iRequestingThumbnailId].saveId; + + // Read thumbnail.png from the save folder + char thumbPath[MAX_PATH]; + sprintf_s(thumbPath, MAX_PATH, "Windows64\\GameHDD\\%s\\thumbnail.png", + pSaveDetails->SaveInfoA[saveId].UTF8SaveFilename); + + HANDLE hFile = CreateFileA(thumbPath, GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr); + if (hFile != INVALID_HANDLE_VALUE) + { + DWORD fileSize = GetFileSize(hFile, nullptr); + if (fileSize > 0 && fileSize != INVALID_FILE_SIZE && fileSize < 4 * 1024 * 1024) + { + PBYTE fileData = new BYTE[fileSize]; + DWORD bytesRead = 0; + if (ReadFile(hFile, fileData, fileSize, &bytesRead, nullptr) && bytesRead == fileSize) + { + m_saveDetails[m_iRequestingThumbnailId].pbThumbnailData = fileData; + m_saveDetails[m_iRequestingThumbnailId].dwThumbnailSize = fileSize; + } + else + { + delete[] fileData; + } + } + CloseHandle(hFile); + } + + // register the texture if we got one + wchar_t wFilename[MAX_SAVEFILENAME_LENGTH]; + ZeroMemory(wFilename, sizeof(wFilename)); + mbstowcs(wFilename, m_saveDetails[m_iRequestingThumbnailId].UTF8SaveFilename, MAX_SAVEFILENAME_LENGTH - 1); + + if (m_saveDetails[m_iRequestingThumbnailId].pbThumbnailData) + { + registerSubstitutionTexture(wFilename, + m_saveDetails[m_iRequestingThumbnailId].pbThumbnailData, + m_saveDetails[m_iRequestingThumbnailId].dwThumbnailSize); + } + m_buttonListSaves.setTextureName(m_iRequestingThumbnailId + m_iDefaultButtonsC, wFilename); + + ++m_iRequestingThumbnailId; + } + m_bAllLoaded = true; + } +#else if(!m_bExitScene && m_bSavesDisplayed && !m_bRetrievingSaveThumbnails && !m_bAllLoaded) { if( m_iRequestingThumbnailId < (m_buttonListSaves.getItemCount() - m_iDefaultButtonsC )) @@ -830,11 +887,7 @@ void UIScene_LoadOrJoinMenu::tick() app.DebugPrintf("Requesting the first thumbnail\n"); // set the save to load PSAVE_DETAILS pSaveDetails=StorageManager.ReturnSavesInfo(); -#ifdef _WINDOWS64 - C4JStorage::ESaveGameState eLoadStatus=StorageManager.LoadSaveDataThumbnail(&pSaveDetails->SaveInfoA[m_saveDetails[m_iRequestingThumbnailId].saveId],&LoadSaveDataThumbnailReturned,this); -#else C4JStorage::ESaveGameState eLoadStatus=StorageManager.LoadSaveDataThumbnail(&pSaveDetails->SaveInfoA[(int)m_iRequestingThumbnailId],&LoadSaveDataThumbnailReturned,this); -#endif if(eLoadStatus!=C4JStorage::ESaveGame_GetSaveThumbnail) { @@ -856,16 +909,6 @@ void UIScene_LoadOrJoinMenu::tick() #ifdef _DURANGO // Already utf16 on durango memcpy(u16Message, m_saveDetails[m_iRequestingThumbnailId].UTF16SaveFilename, MAX_SAVEFILENAME_LENGTH); -#elif defined(_WINDOWS64) - int result = ::MultiByteToWideChar( - CP_UTF8, // convert from UTF-8 - MB_ERR_INVALID_CHARS, // error on invalid chars - m_saveDetails[m_iRequestingThumbnailId].UTF8SaveFilename, // source UTF-8 string - MAX_SAVEFILENAME_LENGTH, // total length of source UTF-8 string, - // in CHAR's (= bytes), including end-of-string \0 - (wchar_t *)u16Message, // destination buffer - MAX_SAVEFILENAME_LENGTH // size of destination buffer, in WCHAR's - ); #else #ifdef __PS3 size_t srcmax,dstmax; @@ -897,11 +940,7 @@ void UIScene_LoadOrJoinMenu::tick() app.DebugPrintf("Requesting another thumbnail\n"); // set the save to load PSAVE_DETAILS pSaveDetails=StorageManager.ReturnSavesInfo(); -#ifdef _WINDOWS64 - C4JStorage::ESaveGameState eLoadStatus=StorageManager.LoadSaveDataThumbnail(&pSaveDetails->SaveInfoA[m_saveDetails[m_iRequestingThumbnailId].saveId],&LoadSaveDataThumbnailReturned,this); -#else C4JStorage::ESaveGameState eLoadStatus=StorageManager.LoadSaveDataThumbnail(&pSaveDetails->SaveInfoA[(int)m_iRequestingThumbnailId],&LoadSaveDataThumbnailReturned,this); -#endif if(eLoadStatus!=C4JStorage::ESaveGame_GetSaveThumbnail) { // something went wrong @@ -921,6 +960,7 @@ void UIScene_LoadOrJoinMenu::tick() m_bRetrievingSaveThumbnails = false; } } +#endif } switch(m_iState) diff --git a/Minecraft.Client/Windows64/Windows64_App.cpp b/Minecraft.Client/Windows64/Windows64_App.cpp index d95aa7f62..6ce0cc9fd 100644 --- a/Minecraft.Client/Windows64/Windows64_App.cpp +++ b/Minecraft.Client/Windows64/Windows64_App.cpp @@ -36,6 +36,225 @@ void CConsoleMinecraftApp::FatalLoadError() void CConsoleMinecraftApp::CaptureSaveThumbnail() { RenderManager.CaptureThumbnail(&m_ThumbnailBuffer); + + // CaptureThumbnail in the render lib doesn't produce data on Win64, + // so grab the back buffer via D3D11 and encode a PNG manually + if (!m_ThumbnailBuffer.Allocated()) + { + extern ID3D11Device* g_pd3dDevice; + extern ID3D11DeviceContext* g_pImmediateContext; + extern IDXGISwapChain* g_pSwapChain; + + if (!g_pd3dDevice || !g_pImmediateContext || !g_pSwapChain) + return; + + ID3D11Texture2D* pBackBuffer = nullptr; + HRESULT hr = g_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBuffer); + if (FAILED(hr) || !pBackBuffer) + return; + + D3D11_TEXTURE2D_DESC bbDesc; + pBackBuffer->GetDesc(&bbDesc); + + // 64x64 to match other platforms + const int THUMB_W = 64; + const int THUMB_H = 64; + + // staging texture for CPU readback + D3D11_TEXTURE2D_DESC stagingDesc = {}; + stagingDesc.Width = bbDesc.Width; + stagingDesc.Height = bbDesc.Height; + stagingDesc.MipLevels = 1; + stagingDesc.ArraySize = 1; + stagingDesc.Format = bbDesc.Format; + stagingDesc.SampleDesc.Count = 1; + stagingDesc.Usage = D3D11_USAGE_STAGING; + stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + + ID3D11Texture2D* pStaging = nullptr; + + // resolve MSAA if needed + if (bbDesc.SampleDesc.Count > 1) + { + D3D11_TEXTURE2D_DESC resolveDesc = bbDesc; + resolveDesc.SampleDesc.Count = 1; + resolveDesc.SampleDesc.Quality = 0; + resolveDesc.Usage = D3D11_USAGE_DEFAULT; + resolveDesc.BindFlags = 0; + resolveDesc.CPUAccessFlags = 0; + + ID3D11Texture2D* pResolved = nullptr; + hr = g_pd3dDevice->CreateTexture2D(&resolveDesc, nullptr, &pResolved); + if (SUCCEEDED(hr)) + { + g_pImmediateContext->ResolveSubresource(pResolved, 0, pBackBuffer, 0, bbDesc.Format); + hr = g_pd3dDevice->CreateTexture2D(&stagingDesc, nullptr, &pStaging); + if (SUCCEEDED(hr)) + g_pImmediateContext->CopyResource(pStaging, pResolved); + pResolved->Release(); + } + } + else + { + hr = g_pd3dDevice->CreateTexture2D(&stagingDesc, nullptr, &pStaging); + if (SUCCEEDED(hr)) + g_pImmediateContext->CopyResource(pStaging, pBackBuffer); + } + pBackBuffer->Release(); + + if (!pStaging) + return; + + D3D11_MAPPED_SUBRESOURCE mapped; + hr = g_pImmediateContext->Map(pStaging, 0, D3D11_MAP_READ, 0, &mapped); + if (FAILED(hr)) + { + pStaging->Release(); + return; + } + + // nearest-neighbor downsample to 64x64 + BYTE* thumbPixels = new BYTE[THUMB_W * THUMB_H * 4]; + for (int y = 0; y < THUMB_H; y++) + { + int srcY = y * (int)bbDesc.Height / THUMB_H; + BYTE* srcRow = (BYTE*)mapped.pData + srcY * mapped.RowPitch; + for (int x = 0; x < THUMB_W; x++) + { + int srcX = x * (int)bbDesc.Width / THUMB_W; + BYTE* src = srcRow + srcX * 4; // assumes BGRA/RGBA 32bpp + BYTE* dst = thumbPixels + (y * THUMB_W + x) * 4; + dst[0] = src[2]; // R (from B in BGRA) + dst[1] = src[1]; // G + dst[2] = src[0]; // B (from R in BGRA) + dst[3] = 255; // A + } + } + + g_pImmediateContext->Unmap(pStaging, 0); + pStaging->Release(); + + // encode uncompressed PNG (IHDR + stored IDAT + IEND) + int rawRowSize = 1 + THUMB_W * 3; // filter byte + RGB + int rawSize = rawRowSize * THUMB_H; + + // single stored deflate block (rawSize fits in 16 bits) + int deflateSize = 5 + rawSize; // 5-byte block header + data + if (rawSize > 65535) + { + // shouldn't happen at 64x64 but bail just in case + delete[] thumbPixels; + return; + } + + int idatDataSize = 2 + deflateSize + 4; // zlib header(2) + deflate + adler32(4) + int pngSize = 8 + (12 + 13) + (12 + idatDataSize) + 12; // sig + IHDR + IDAT + IEND + + BYTE* png = (BYTE*)malloc(pngSize); + if (!png) + { + delete[] thumbPixels; + return; + } + int pos = 0; + + // PNG signature + static const BYTE sig[8] = {0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A}; + memcpy(png + pos, sig, 8); pos += 8; + + // big-endian write helper + auto writeBE32 = [&](int offset, unsigned int val) { + png[offset+0] = (val >> 24) & 0xFF; + png[offset+1] = (val >> 16) & 0xFF; + png[offset+2] = (val >> 8) & 0xFF; + png[offset+3] = (val ) & 0xFF; + }; + + // CRC32 for PNG chunks + static unsigned int crcTable[256]; + static bool crcInit = false; + if (!crcInit) + { + for (int n = 0; n < 256; n++) + { + unsigned int c = n; + for (int k = 0; k < 8; k++) + c = (c & 1) ? 0xEDB88320 ^ (c >> 1) : c >> 1; + crcTable[n] = c; + } + crcInit = true; + } + auto crc32 = [&](BYTE* data, int len) -> unsigned int { + unsigned int c = 0xFFFFFFFF; + for (int i = 0; i < len; i++) + c = crcTable[(c ^ data[i]) & 0xFF] ^ (c >> 8); + return c ^ 0xFFFFFFFF; + }; + + // IHDR chunk + writeBE32(pos, 13); pos += 4; // length + int ihdrStart = pos; + png[pos++]='I'; png[pos++]='H'; png[pos++]='D'; png[pos++]='R'; + writeBE32(pos, THUMB_W); pos += 4; + writeBE32(pos, THUMB_H); pos += 4; + png[pos++] = 8; // bit depth + png[pos++] = 2; // color type RGB + png[pos++] = 0; // compression + png[pos++] = 0; // filter + png[pos++] = 0; // interlace + writeBE32(pos, crc32(png + ihdrStart, 17)); pos += 4; + + // IDAT chunk + writeBE32(pos, idatDataSize); pos += 4; + int idatStart = pos; + png[pos++]='I'; png[pos++]='D'; png[pos++]='A'; png[pos++]='T'; + + // zlib header (no compression) + png[pos++] = 0x78; png[pos++] = 0x01; + + // Deflate stored block (final) + png[pos++] = 0x01; // final block, stored + png[pos++] = (rawSize ) & 0xFF; + png[pos++] = (rawSize >> 8) & 0xFF; + png[pos++] = ~rawSize & 0xFF; + png[pos++] = (~rawSize >> 8) & 0xFF; + + // Raw PNG data (filter=None for each row, RGB) + unsigned int adlerA = 1, adlerB = 0; + for (int y = 0; y < THUMB_H; y++) + { + png[pos++] = 0; // filter byte = None + adlerA = (adlerA + 0) % 65521; + adlerB = (adlerB + adlerA) % 65521; + for (int x = 0; x < THUMB_W; x++) + { + BYTE* px = thumbPixels + (y * THUMB_W + x) * 4; + for (int c = 0; c < 3; c++) + { + png[pos++] = px[c]; + adlerA = (adlerA + px[c]) % 65521; + adlerB = (adlerB + adlerA) % 65521; + } + } + } + // Adler32 + unsigned int adler = (adlerB << 16) | adlerA; + writeBE32(pos, adler); pos += 4; + // IDAT CRC + writeBE32(pos, crc32(png + idatStart, 4 + idatDataSize)); pos += 4; + + // IEND chunk + writeBE32(pos, 0); pos += 4; + int iendStart = pos; + png[pos++]='I'; png[pos++]='E'; png[pos++]='N'; png[pos++]='D'; + writeBE32(pos, crc32(png + iendStart, 4)); pos += 4; + + delete[] thumbPixels; + + m_ThumbnailBuffer.m_type = ImageFileBuffer::e_typePNG; + m_ThumbnailBuffer.m_pBuffer = png; + m_ThumbnailBuffer.m_bufferSize = pos; + } } void CConsoleMinecraftApp::GetSaveThumbnail(PBYTE *pbData,DWORD *pdwSize) { diff --git a/Minecraft.World/ConsoleSaveFileOriginal.cpp b/Minecraft.World/ConsoleSaveFileOriginal.cpp index 8d6571b22..25a1d0acc 100644 --- a/Minecraft.World/ConsoleSaveFileOriginal.cpp +++ b/Minecraft.World/ConsoleSaveFileOriginal.cpp @@ -33,6 +33,11 @@ static BackgroundSaveResult s_bgResult; #endif +#ifdef _WINDOWS64 +static PBYTE s_pendingThumbData = nullptr; +static DWORD s_pendingThumbSize = 0; +#endif + #ifdef _XBOX #define RESERVE_ALLOCATION MEM_RESERVE | MEM_LARGE_PAGES #define COMMIT_ALLOCATION MEM_COMMIT | MEM_LARGE_PAGES @@ -869,6 +874,17 @@ void ConsoleSaveFileOriginal::Flush(bool autosave, bool updateThumbnail ) app.GetSaveThumbnail(&pbThumbnailData,&dwThumbnailDataSize,&pbDataSaveImage,&dwDataSizeSaveImage); #endif +#ifdef _WINDOWS64 + // stash the thumbnail so the save callback can write it to disk once + // the storage lib has actually created the folder and saveData.ms + if (pbThumbnailData && dwThumbnailDataSize > 0) + { + s_pendingThumbData = new BYTE[dwThumbnailDataSize]; + memcpy(s_pendingThumbData, pbThumbnailData, dwThumbnailDataSize); + s_pendingThumbSize = dwThumbnailDataSize; + } +#endif + BYTE bTextMetadata[88]; ZeroMemory(bTextMetadata,88); @@ -939,6 +955,62 @@ int ConsoleSaveFileOriginal::SaveSaveDataCallback(LPVOID lpParam,bool bRes) s_bgSaveActive.store(false, std::memory_order_release); #endif +#ifdef _WINDOWS64 + // GetSaveUniqueFilename returns empty on Win64, so find the save folder + // by looking for whichever saveData.ms was written most recently + if (s_pendingThumbData && s_pendingThumbSize > 0) + { + WIN32_FIND_DATAA fd; + HANDLE hFind = FindFirstFileA("Windows64\\GameHDD\\*", &fd); + if (hFind != INVALID_HANDLE_VALUE) + { + FILETIME newestTime = {}; + char newestDir[MAX_PATH] = {}; + + do + { + if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && + fd.cFileName[0] != '.') + { + // Check saveData.ms modification time in this folder + char msPath[MAX_PATH]; + sprintf_s(msPath, MAX_PATH, "Windows64\\GameHDD\\%s\\saveData.ms", fd.cFileName); + WIN32_FIND_DATAA msfd; + HANDLE hMs = FindFirstFileA(msPath, &msfd); + if (hMs != INVALID_HANDLE_VALUE) + { + if (CompareFileTime(&msfd.ftLastWriteTime, &newestTime) > 0) + { + newestTime = msfd.ftLastWriteTime; + strcpy_s(newestDir, MAX_PATH, fd.cFileName); + } + FindClose(hMs); + } + } + } while (FindNextFileA(hFind, &fd)); + FindClose(hFind); + + if (newestDir[0] != '\0') + { + char thumbPath[MAX_PATH]; + sprintf_s(thumbPath, MAX_PATH, "Windows64\\GameHDD\\%s\\thumbnail.png", newestDir); + HANDLE hFile = CreateFileA(thumbPath, GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hFile != INVALID_HANDLE_VALUE) + { + DWORD bytesWritten = 0; + WriteFile(hFile, s_pendingThumbData, s_pendingThumbSize, &bytesWritten, nullptr); + CloseHandle(hFile); + } + } + } + + delete[] s_pendingThumbData; + s_pendingThumbData = nullptr; + s_pendingThumbSize = 0; + } +#endif + return 0; }