Fix game freeze when pressing Play Game with multiple saves

Optimize ReadLevelNameFromSaveFile() to avoid reading entire save files into memory.

For uncompressed saves: use seek-based file I/O to read only the header, file table, and level.dat entry (~100 KB instead of 10-50+ MB).

For compressed saves: use a levelName.cache sidecar file so the full decompression only happens once; subsequent loads read the small cache file instantly.

Previously, the function read and decompressed entire save files on the main thread for every save in a single frame, causing a multi-second freeze that scaled with the number of saves.

Fixes #409
This commit is contained in:
MCbabel 2026-03-04 17:21:39 +01:00
parent 9383b2aa01
commit cb498a917d

View file

@ -29,90 +29,182 @@
#include "..\..\..\Minecraft.World\NbtIo.h" #include "..\..\..\Minecraft.World\NbtIo.h"
#include "..\..\..\Minecraft.World\compression.h" #include "..\..\..\Minecraft.World\compression.h"
// Helper: extract level name from in-memory save data (after decompression or for raw files)
static wstring ExtractLevelNameFromData(unsigned char *saveData, unsigned int saveSize)
{
if (saveSize < 12) return L"";
unsigned int headerOffset = *(unsigned int*)saveData;
unsigned int numEntries = *(unsigned int*)(saveData + 4);
const unsigned int entrySize = sizeof(FileEntrySaveData);
if (headerOffset >= saveSize || numEntries == 0 || numEntries >= 10000 ||
headerOffset + numEntries * entrySize > saveSize)
return L"";
FileEntrySaveData *table = (FileEntrySaveData *)(saveData + headerOffset);
for (unsigned int i = 0; i < numEntries; i++)
{
if (wcscmp(table[i].filename, L"level.dat") == 0)
{
unsigned int off = table[i].startOffset;
unsigned int len = table[i].length;
if (off >= 12 && off + len <= saveSize && len > 0 && len < 4 * 1024 * 1024)
{
byteArray ba;
ba.data = (byte*)(saveData + off);
ba.length = len;
CompoundTag *root = NbtIo::decompress(ba);
if (root != NULL)
{
wstring result = L"";
CompoundTag *dataTag = root->getCompound(L"Data");
if (dataTag != NULL)
result = dataTag->getString(L"LevelName");
delete root;
return result;
}
}
break;
}
}
return L"";
}
// Try to read a cached level name from a sidecar file
static wstring ReadCachedLevelName(const wstring& saveDir)
{
wstring cachePath = saveDir + L"\\levelName.cache";
HANDLE hFile = CreateFileW(cachePath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) return L"";
wchar_t buf[256] = {};
DWORD bytesRead = 0;
ReadFile(hFile, buf, sizeof(buf) - sizeof(wchar_t), &bytesRead, NULL);
CloseHandle(hFile);
if (bytesRead < 2) return L"";
buf[bytesRead / sizeof(wchar_t)] = L'\0';
return wstring(buf);
}
// Write a level name to the sidecar cache file
static void WriteCachedLevelName(const wstring& saveDir, const wstring& name)
{
wstring cachePath = saveDir + L"\\levelName.cache";
HANDLE hFile = CreateFileW(cachePath.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) return;
DWORD written = 0;
WriteFile(hFile, name.c_str(), (DWORD)(name.size() * sizeof(wchar_t)), &written, NULL);
CloseHandle(hFile);
}
static wstring ReadLevelNameFromSaveFile(const wstring& filePath) static wstring ReadLevelNameFromSaveFile(const wstring& filePath)
{ {
HANDLE hFile = CreateFileW(filePath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); // Derive the save directory from the file path (parent of saveData.ms)
wstring saveDir = filePath.substr(0, filePath.find_last_of(L'\\'));
// 1. Check sidecar cache first (instant for repeat loads)
wstring cached = ReadCachedLevelName(saveDir);
if (!cached.empty()) return cached;
// 2. Open the save file
HANDLE hFile = CreateFileW(filePath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_RANDOM_ACCESS, NULL);
if (hFile == INVALID_HANDLE_VALUE) return L""; if (hFile == INVALID_HANDLE_VALUE) return L"";
DWORD fileSize = GetFileSize(hFile, NULL); DWORD fileSize = GetFileSize(hFile, NULL);
if (fileSize < 12 || fileSize == INVALID_FILE_SIZE) { CloseHandle(hFile); return L""; } if (fileSize < 12 || fileSize == INVALID_FILE_SIZE) { CloseHandle(hFile); return L""; }
unsigned char *rawData = new unsigned char[fileSize]; // Read the first 8 bytes to determine format
unsigned char header[8];
DWORD bytesRead = 0; DWORD bytesRead = 0;
if (!ReadFile(hFile, rawData, fileSize, &bytesRead, NULL) || bytesRead != fileSize) if (!ReadFile(hFile, header, 8, &bytesRead, NULL) || bytesRead < 8) { CloseHandle(hFile); return L""; }
{
CloseHandle(hFile);
delete[] rawData;
return L"";
}
CloseHandle(hFile);
unsigned char *saveData = NULL; wstring result = L"";
unsigned int saveSize = 0;
bool freeSaveData = false;
if (*(unsigned int*)rawData == 0) if (*(unsigned int*)header == 0)
{ {
// Compressed format: bytes 0-3=0, bytes 4-7=decompressed size, bytes 8+=compressed data // Compressed save: must read and decompress entire file
unsigned int decompSize = *(unsigned int*)(rawData + 4); SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
if (decompSize == 0 || decompSize > 128 * 1024 * 1024) unsigned char *rawData = new unsigned char[fileSize];
if (!ReadFile(hFile, rawData, fileSize, &bytesRead, NULL) || bytesRead != fileSize)
{ {
CloseHandle(hFile);
delete[] rawData; delete[] rawData;
return L""; return L"";
} }
saveData = new unsigned char[decompSize]; CloseHandle(hFile);
Compression::getCompression()->Decompress(saveData, &decompSize, rawData + 8, fileSize - 8);
saveSize = decompSize; unsigned int decompSize = *(unsigned int*)(rawData + 4);
freeSaveData = true; if (decompSize > 0 && decompSize <= 128 * 1024 * 1024)
{
unsigned char *saveData = new unsigned char[decompSize];
Compression::getCompression()->Decompress(saveData, &decompSize, rawData + 8, fileSize - 8);
result = ExtractLevelNameFromData(saveData, decompSize);
delete[] saveData;
}
delete[] rawData;
} }
else else
{ {
saveData = rawData; // Uncompressed save: use seek-based reading (fast path)
saveSize = fileSize; // Read header: headerOffset and numEntries
} unsigned int headerOffset = *(unsigned int*)header;
unsigned int numEntries = *(unsigned int*)(header + 4);
wstring result = L"";
if (saveSize >= 12)
{
unsigned int headerOffset = *(unsigned int*)saveData;
unsigned int numEntries = *(unsigned int*)(saveData + 4);
const unsigned int entrySize = sizeof(FileEntrySaveData); const unsigned int entrySize = sizeof(FileEntrySaveData);
if (headerOffset < saveSize && numEntries > 0 && numEntries < 10000 && if (headerOffset < fileSize && numEntries > 0 && numEntries < 10000 &&
headerOffset + numEntries * entrySize <= saveSize) headerOffset + numEntries * entrySize <= fileSize)
{ {
FileEntrySaveData *table = (FileEntrySaveData *)(saveData + headerOffset); // Seek to file table and read only the table
for (unsigned int i = 0; i < numEntries; i++) SetFilePointer(hFile, headerOffset, NULL, FILE_BEGIN);
unsigned int tableSize = numEntries * entrySize;
unsigned char *tableData = new unsigned char[tableSize];
if (ReadFile(hFile, tableData, tableSize, &bytesRead, NULL) && bytesRead == tableSize)
{ {
if (wcscmp(table[i].filename, L"level.dat") == 0) FileEntrySaveData *table = (FileEntrySaveData *)tableData;
for (unsigned int i = 0; i < numEntries; i++)
{ {
unsigned int off = table[i].startOffset; if (wcscmp(table[i].filename, L"level.dat") == 0)
unsigned int len = table[i].length;
if (off >= 12 && off + len <= saveSize && len > 0 && len < 4 * 1024 * 1024)
{ {
byteArray ba; unsigned int off = table[i].startOffset;
ba.data = (byte*)(saveData + off); unsigned int len = table[i].length;
ba.length = len; if (off >= 12 && off + len <= fileSize && len > 0 && len < 4 * 1024 * 1024)
CompoundTag *root = NbtIo::decompress(ba);
if (root != NULL)
{ {
CompoundTag *dataTag = root->getCompound(L"Data"); // Seek to level.dat data and read only that
if (dataTag != NULL) SetFilePointer(hFile, off, NULL, FILE_BEGIN);
result = dataTag->getString(L"LevelName"); unsigned char *levelData = new unsigned char[len];
delete root; DWORD levelRead = 0;
if (ReadFile(hFile, levelData, len, &levelRead, NULL) && levelRead == len)
{
byteArray ba;
ba.data = (byte*)levelData;
ba.length = len;
CompoundTag *root = NbtIo::decompress(ba);
if (root != NULL)
{
CompoundTag *dataTag = root->getCompound(L"Data");
if (dataTag != NULL)
result = dataTag->getString(L"LevelName");
delete root;
}
}
delete[] levelData;
} }
break;
} }
break;
} }
} }
delete[] tableData;
} }
CloseHandle(hFile);
} }
if (freeSaveData) delete[] saveData; // "world" is the engine default — return empty to fall back to timestamp
delete[] rawData;
// "world" is the engine default — it means no real name was ever set, so
// return empty to let the caller fall back to the save filename (timestamp).
if (result == L"world") result = L""; if (result == L"world") result = L"";
// Cache the result for future loads
if (!result.empty())
WriteCachedLevelName(saveDir, result);
return result; return result;
} }
#endif #endif