From 5dd69bfdf9db785ce72f4322a8edcdb2f3c7fcde Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 8 Sep 2026 21:28:55 +0200 Subject: [PATCH] feat: add LayoutMeasure and measureLayout --- core/src/RegionCompact.cpp | 159 +++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 core/src/RegionCompact.cpp diff --git a/core/src/RegionCompact.cpp b/core/src/RegionCompact.cpp new file mode 100644 index 0000000..3841d6c --- /dev/null +++ b/core/src/RegionCompact.cpp @@ -0,0 +1,159 @@ +#include "vf/RegionCompact.h" + +#include + +namespace vf { + +namespace { + +std::size_t divideUp(std::size_t value, std::size_t unit) { + return unit == 0 ? 0 : (value + unit - 1) / unit; +} + +struct LayoutMeasure { + bool ok = false; + SaveHeader header; + std::vector entries; + uint32_t totalSectors = 0; + uint32_t headerSectors = 0; + uint32_t referencedSectors = 0; + uint32_t holeSectors = 0; + uint32_t excessSectors = 0; + uint32_t slackBytes = 0; +}; + +LayoutMeasure measureLayout(const uint8_t* data, std::size_t size) { + LayoutMeasure measure; + if (!readSaveHeader(data, size, measure.header)) return measure; + if (measure.header.magic != saveMagic) return measure; + if (!isKnownSaveVersion(measure.header.version)) return measure; + if (measure.header.sectorBytes == 0) return measure; + if (!readSaveTable(data, size, measure.header, measure.entries)) return measure; + + uint32_t sectorBytes = measure.header.sectorBytes; + measure.totalSectors = static_cast(divideUp(size, sectorBytes)); + measure.headerSectors = static_cast(saveHeaderSectors(measure.header)); + + for (const SaveTableEntry& entry : measure.entries) { + measure.referencedSectors += entry.sectorCount; + uint32_t needed = + static_cast(divideUp(entry.payloadBytes, sectorBytes)); + if (entry.sectorCount > needed) measure.excessSectors += entry.sectorCount - needed; + uint32_t runBytes = entry.sectorCount * sectorBytes; + if (runBytes > entry.payloadBytes) measure.slackBytes += runBytes - entry.payloadBytes; + } + + uint32_t accounted = measure.headerSectors + measure.referencedSectors; + measure.holeSectors = measure.totalSectors > accounted ? measure.totalSectors - accounted : 0; + measure.ok = true; + return measure; +} + +void rewriteTableChecksum(ByteBuffer& buffer, const SaveHeader& header) { + if (!versionHasChecksum(static_cast(header.version))) return; + std::size_t tableBytes = saveTableBytes(header); + uint32_t checksum = fnv1a32(buffer.data() + saveHeaderBytes, tableBytes); + saveWriteU32At(buffer.data(), buffer.size(), 24, checksum); +} + +} // namespace + +bool updateChunkInPlace(ByteBuffer& buffer, const SaveChunkRecord& record, bool compressBody) { + SaveHeader header; + if (!readSaveHeader(buffer.data(), buffer.size(), header)) return false; + if (header.magic != saveMagic || !isKnownSaveVersion(header.version)) return false; + if (header.sectorBytes == 0) return false; + + std::vector entries; + if (!readSaveTable(buffer.data(), buffer.size(), header, entries)) return false; + + std::size_t slot = entries.size(); + for (std::size_t i = 0; i < entries.size(); ++i) + if (entries[i].coord == record.coord) slot = i; + if (slot == entries.size()) return false; + + SaveVersion version = static_cast(header.version); + ByteBuffer payload = encodeChunkPayload(version, record, compressBody); + uint32_t sectorBytes = header.sectorBytes; + uint32_t needed = static_cast(divideUp(payload.size(), sectorBytes)); + std::size_t entryAt = saveHeaderBytes + slot * saveEntryBytes; + + if (needed <= entries[slot].sectorCount) { + std::size_t at = static_cast(entries[slot].sectorOffset) * sectorBytes; + std::size_t runBytes = static_cast(entries[slot].sectorCount) * sectorBytes; + if (at + runBytes > buffer.size()) return false; + std::fill(buffer.begin() + static_cast(at), + buffer.begin() + static_cast(at + runBytes), uint8_t{0}); + std::copy(payload.begin(), payload.end(), + buffer.begin() + static_cast(at)); + saveWriteU32At(buffer.data(), buffer.size(), entryAt + 20, + static_cast(payload.size())); + rewriteTableChecksum(buffer, header); + return true; + } + + if (buffer.size() % sectorBytes != 0) + buffer.resize(divideUp(buffer.size(), sectorBytes) * sectorBytes, 0); + uint32_t newOffset = static_cast(buffer.size() / sectorBytes); + buffer.resize(buffer.size() + static_cast(needed) * sectorBytes, 0); + std::copy(payload.begin(), payload.end(), + buffer.begin() + static_cast( + static_cast(newOffset) * sectorBytes)); + saveWriteU32At(buffer.data(), buffer.size(), entryAt + 12, newOffset); + saveWriteU32At(buffer.data(), buffer.size(), entryAt + 16, needed); + saveWriteU32At(buffer.data(), buffer.size(), entryAt + 20, + static_cast(payload.size())); + rewriteTableChecksum(buffer, header); + return true; +} + +std::vector RegionCompactor::survey(const ByteBuffer& buffer) const { + return survey(buffer.data(), buffer.size()); +} + +std::vector RegionCompactor::survey(const uint8_t* data, std::size_t size) const { + std::vector fragments; + LayoutMeasure measure = measureLayout(data, size); + if (!measure.ok) return fragments; + + uint32_t sectorBytes = measure.header.sectorBytes; + SaveVersion version = static_cast(measure.header.version); + for (std::size_t i = 0; i < measure.entries.size(); ++i) { + const SaveTableEntry& entry = measure.entries[i]; + ChunkFragment fragment; + fragment.coord = entry.coord; + fragment.entryIndex = static_cast(i); + fragment.sectorOffset = entry.sectorOffset; + fragment.sectorCount = entry.sectorCount; + fragment.payloadBytes = entry.payloadBytes; + fragment.neededSectors = static_cast(divideUp(entry.payloadBytes, sectorBytes)); + fragment.excessSectors = entry.sectorCount > fragment.neededSectors + ? entry.sectorCount - fragment.neededSectors + : 0; + uint32_t runBytes = entry.sectorCount * sectorBytes; + fragment.slackBytes = runBytes > entry.payloadBytes ? runBytes - entry.payloadBytes : 0; + + std::size_t at = static_cast(entry.sectorOffset) * sectorBytes; + fragment.reachable = + entry.sectorCount != 0 && at + entry.payloadBytes <= size && at < size; + if (fragment.reachable && versionHasMetadata(version)) { + uint32_t flags = 0; + if (saveReadU32At(data, size, at, flags)) + fragment.empty = (flags & saveRecordEmpty) != 0; + } + fragments.push_back(fragment); + } + return fragments; +} + +int RegionCompactor::fragmentationPercent(const ByteBuffer& buffer) const { + return fragmentationPercent(buffer.data(), buffer.size()); +} + +int RegionCompactor::fragmentationPercent(const uint8_t* data, std::size_t size) const { + LayoutMeasure measure = measureLayout(data, size); + if (!measure.ok || measure.totalSectors == 0) return -1; + uint32_t reclaimable = measure.holeSectors + measure.excessSectors; + return static_cast((reclaimable * 100u) / measure.totalSectors); +} +