Optimize ZIP resource packs significantly

This commit is contained in:
embeddedt 2026-05-23 21:23:26 -04:00
parent 538c52bc2a
commit 62dbbea083
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
2 changed files with 406 additions and 0 deletions

View File

@ -0,0 +1,95 @@
package org.embeddedt.modernfix.common.mixin.perf.resourcepacks;
import net.minecraft.server.packs.FilePackResources;
import net.minecraft.server.packs.PackResources;
import net.minecraft.server.packs.PackType;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.FeatureLevel;
import org.embeddedt.modernfix.annotation.RequiresFeatureLevel;
import org.embeddedt.modernfix.resources.ZipPackIndex;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.io.File;
import java.io.IOException;
import java.util.Set;
import java.util.zip.ZipFile;
@Mixin(FilePackResources.class)
@RequiresFeatureLevel(FeatureLevel.BETA)
public class FilePackResourcesMixin {
@Final
@Shadow private File file;
@Shadow @Nullable private ZipFile getOrCreateZipFile() { return null; }
@Unique
@Nullable
private volatile ZipPackIndex mf$packIndex;
@Unique
@Nullable
private ZipPackIndex mf$getOrCreateIndex() {
var index = mf$packIndex;
if (index == null) {
synchronized (this) {
index = mf$packIndex;
if (index == null) {
// Ensure the ZipFile is open first; if it fails, getOrCreateZipFile returns null.
if (getOrCreateZipFile() == null) {
return null;
}
try {
mf$packIndex = index = new ZipPackIndex(file.toPath());
} catch (IOException e) {
ModernFix.LOGGER.error("Failed to build zip index for {}", file, e);
}
}
}
}
return index;
}
/**
* @author embeddedt
* @reason use the index instead of scanning the whole zip
*/
@Inject(method = "getNamespaces", at = @At("HEAD"), cancellable = true)
private void mf$getNamespaces(PackType type, CallbackInfoReturnable<Set<String>> cir) {
ZipPackIndex index = mf$getOrCreateIndex();
if (index != null) {
cir.setReturnValue(index.getNamespaces(type));
}
}
/**
* @author embeddedt
* @reason use the index instead of scanning the whole zip
*/
@Inject(method = "listResources", at = @At("HEAD"), cancellable = true)
private void mf$listResources(PackType packType, String namespace, String path,
PackResources.ResourceOutput resourceOutput, CallbackInfo ci) {
ZipFile zf = getOrCreateZipFile();
ZipPackIndex index = mf$getOrCreateIndex();
if (index != null && zf != null) {
index.listResources(packType, namespace, path, zf, resourceOutput);
ci.cancel();
}
}
/**
* Drop the index when the pack is closed so it can be rebuilt cleanly if the
* pack is ever re-opened.
*/
@Inject(method = "close", at = @At("HEAD"))
private void mf$invalidateIndex(CallbackInfo ci) {
mf$packIndex = null;
}
}

View File

@ -0,0 +1,311 @@
package org.embeddedt.modernfix.resources;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.PackResources;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.resources.IoSupplier;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* An index over a zip file's central directory that allows efficient namespace listing
* and resource enumeration without iterating all entries on every call.
*
* <p>The index is built once at construction time by memory-mapping the zip's central
* directory and parsing it into a {@link DirNode} tree. All subsequent queries run in
* O(depth + k) time where k is the number of matching results.
*
* <p>The caller is responsible for opening and closing the {@link ZipFile}; this class
* only holds a read-only view of the zip's metadata via a mmap'd buffer.
*/
public class ZipPackIndex {
// -------------------------------------------------------------------------
// Zip structural constants (identical to EfficientZipFileSystem in blacksmith)
// -------------------------------------------------------------------------
private static final int EOCD_SIGNATURE = 0x06054b50;
private static final int EOCD_SIZE = 22;
private static final int EOCD_OFF_CD_SIZE = 12;
private static final int EOCD_OFF_CD_OFFSET = 16;
private static final int EOCD_MAX_COMMENT_LENGTH = 65535;
private static final int CD_ENTRY_SIGNATURE = 0x02014b50;
private static final int CD_ENTRY_HEADER_SIZE = 46;
private static final int CD_OFF_FILENAME_LENGTH = 28;
private static final int CD_OFF_EXTRA_LENGTH = 30;
private static final int CD_OFF_COMMENT_LENGTH = 32;
private static final int[] EMPTY_OFFSETS = new int[0];
// -------------------------------------------------------------------------
// DirNode
// -------------------------------------------------------------------------
static final class DirNode {
Map<String, DirNode> childDirs;
int[] fileChildOffsets; // offsets into cdBuffer for each direct file child
DirNode() {
childDirs = new HashMap<>();
fileChildOffsets = EMPTY_OFFSETS;
}
void freeze() {
childDirs = childDirs.isEmpty() ? Map.of() : Map.copyOf(childDirs);
for (DirNode child : childDirs.values()) {
child.freeze();
}
}
}
// -------------------------------------------------------------------------
// Fields
// -------------------------------------------------------------------------
/** Memory-mapped central directory. May be null for empty/invalid zips. */
private final MappedByteBuffer cdBuffer;
/** Root of the directory tree, always non-null (may be empty but frozen). */
private final DirNode root;
// -------------------------------------------------------------------------
// Construction
// -------------------------------------------------------------------------
/**
* Build an index from the zip at the given path. Does not open a {@link ZipFile}
* and does not keep a reference to one; the caller owns all {@link ZipFile} lifecycle.
*
* @throws IOException if the file cannot be read or its central directory cannot be mapped
*/
public ZipPackIndex(Path zipPath) throws IOException {
this.cdBuffer = mmapCentralDirectory(zipPath);
this.root = buildTree();
}
private static MappedByteBuffer mmapCentralDirectory(Path filePath) throws IOException {
try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ)) {
long fileSize = channel.size();
if (fileSize < EOCD_SIZE) return null;
int tailSize = (int) Math.min(fileSize, (long) EOCD_SIZE + EOCD_MAX_COMMENT_LENGTH);
ByteBuffer tail = ByteBuffer.allocate(tailSize);
tail.order(ByteOrder.LITTLE_ENDIAN);
long tailStart = fileSize - tailSize;
while (tail.hasRemaining()) {
int n = channel.read(tail, tailStart + tail.position());
if (n < 0) {
break;
}
}
if (tail.hasRemaining()) {
throw new IOException("Failed to read ZIP tail");
}
tail.flip();
// Scan backwards for the EOCD signature and validate comment length.
int eocdPos = -1;
for (int i = tailSize - EOCD_SIZE; i >= 0; i--) {
if (tail.getInt(i) == EOCD_SIGNATURE) {
int commentLen = Short.toUnsignedInt(tail.getShort(i + 20));
if (i + EOCD_SIZE + commentLen == tailSize) {
eocdPos = i;
break;
}
}
}
if (eocdPos < 0) return null;
long cdSize = Integer.toUnsignedLong(tail.getInt(eocdPos + EOCD_OFF_CD_SIZE));
long cdOffset = Integer.toUnsignedLong(tail.getInt(eocdPos + EOCD_OFF_CD_OFFSET));
if (cdSize == 0) return null;
if (cdSize == 0xFFFFFFFFL || cdOffset == 0xFFFFFFFFL) {
throw new IOException("ZIP64 not supported by ZipPackIndex");
}
if (cdOffset > fileSize - cdSize) {
throw new IOException("Invalid central directory range");
}
try {
MappedByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, cdOffset, cdSize);
buf.order(ByteOrder.LITTLE_ENDIAN);
return buf;
} catch (RuntimeException e) {
throw new IOException("Failed to map central directory", e);
}
}
}
private DirNode buildTree() throws IOException {
DirNode treeRoot = new DirNode();
if (cdBuffer == null) {
treeRoot.freeze();
return treeRoot;
}
// Accumulate file offsets per DirNode before compacting to int[]
IdentityHashMap<DirNode, List<Integer>> fileOffsets = new IdentityHashMap<>();
int pos = 0;
int limit = cdBuffer.limit();
while (pos + CD_ENTRY_HEADER_SIZE <= limit) {
if (cdBuffer.getInt(pos) != CD_ENTRY_SIGNATURE) break;
int fileNameLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_FILENAME_LENGTH));
int extraLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_EXTRA_LENGTH));
int commentLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_COMMENT_LENGTH));
int recordLen = CD_ENTRY_HEADER_SIZE + fileNameLen + extraLen + commentLen;
if (pos + recordLen > limit) {
throw new IOException("Truncated central directory");
}
byte[] nameBytes = new byte[fileNameLen];
cdBuffer.get(pos + CD_ENTRY_HEADER_SIZE, nameBytes);
String name = new String(nameBytes, StandardCharsets.UTF_8);
boolean isDirectory = name.endsWith("/");
if (isDirectory) name = name.substring(0, name.length() - 1);
if (!name.isEmpty()) {
String[] parts = name.split("/");
DirNode current = treeRoot;
int dirDepth = isDirectory ? parts.length : parts.length - 1;
for (int i = 0; i < dirDepth; i++) {
current = current.childDirs.computeIfAbsent(parts[i], k -> new DirNode());
}
if (!isDirectory) {
fileOffsets.computeIfAbsent(current, k -> new ArrayList<>()).add(pos);
}
}
pos += recordLen;
}
// Compact to int[] arrays
fileOffsets.forEach((node, offsets) -> {
int[] arr = new int[offsets.size()];
for (int i = 0; i < arr.length; i++) arr[i] = offsets.get(i);
node.fileChildOffsets = arr;
});
treeRoot.freeze();
return treeRoot;
}
// -------------------------------------------------------------------------
// CD buffer reads absolute-position gets are thread-safe on Java 13+
// -------------------------------------------------------------------------
/**
* Extract the basename (the portion after the last '/') of the entry whose
* central-directory record starts at {@code cdOffset}.
*/
String readBasename(int cdOffset) {
int nameLen = Short.toUnsignedInt(cdBuffer.getShort(cdOffset + CD_OFF_FILENAME_LENGTH));
byte[] nameBytes = new byte[nameLen];
cdBuffer.get(cdOffset + CD_ENTRY_HEADER_SIZE, nameBytes);
int lastSlash = -1;
for (int i = nameBytes.length - 1; i >= 0; i--) {
if (nameBytes[i] == '/') { lastSlash = i; break; }
}
return new String(nameBytes, lastSlash + 1, nameLen - lastSlash - 1, StandardCharsets.UTF_8);
}
// -------------------------------------------------------------------------
// Public API
// -------------------------------------------------------------------------
/**
* Returns all namespaces present under the given pack type directory.
*
* <p>Equivalent to {@code FilePackResources.getNamespaces(type)} but reads from
* the pre-built tree rather than scanning all zip entries.
*/
public Set<String> getNamespaces(PackType type) {
DirNode typeNode = root.childDirs.get(type.getDirectory());
if (typeNode == null) return Set.of();
Set<String> result = new HashSet<>();
for (String ns : typeNode.childDirs.keySet()) {
if (ns.equals(ns.toLowerCase(Locale.ROOT))) {
result.add(ns);
}
}
return result;
}
/**
* Enumerate all resources under {@code type/namespace/path/} and deliver them
* to {@code output}.
*
* <p>Equivalent to {@code FilePackResources.listResources(type, namespace, path, output)}
* but uses the pre-built tree for O(k) traversal instead of a full zip scan.
*
* @param zipFile the open zip file, used only to supply {@link InputStream}s on demand;
* the caller retains ownership of its lifecycle
*/
public void listResources(PackType type, String namespace, String path,
ZipFile zipFile, PackResources.ResourceOutput output) {
DirNode node = root.childDirs.get(type.getDirectory());
if (node == null) return;
node = node.childDirs.get(namespace);
if (node == null) return;
// Walk to the requested sub-path
String rlSubPath;
if (!path.isEmpty()) {
for (String segment : path.split("/")) {
if (segment.isEmpty()) continue;
node = node.childDirs.get(segment);
if (node == null) return;
}
rlSubPath = path + "/";
} else {
rlSubPath = "";
}
// entryPrefix = the part of the zip entry name before the ResourceLocation path
String entryPrefix = type.getDirectory() + "/" + namespace + "/";
collectResources(node, entryPrefix, rlSubPath, zipFile, namespace, output);
}
/**
* Recursively walk {@code node}, reconstructing zip entry names as we go and
* emitting each file to {@code output}.
*
* @param entryPrefix the constant prefix before the RL path, e.g. {@code "assets/minecraft/"}
* @param rlSubPath the RL-relative path accumulated so far, e.g. {@code "textures/block/"}
*/
private void collectResources(DirNode node, String entryPrefix, String rlSubPath,
ZipFile zipFile, String namespace,
PackResources.ResourceOutput output) {
// Emit direct file children of this node
for (int cdOffset : node.fileChildOffsets) {
String basename = readBasename(cdOffset);
String rlPathFull = rlSubPath + basename;
ResourceLocation rl = ResourceLocation.tryBuild(namespace, rlPathFull);
if (rl != null) {
ZipEntry entry = zipFile.getEntry(entryPrefix + rlPathFull);
if (entry != null) {
output.accept(rl, IoSupplier.create(zipFile, entry));
}
}
}
// Recurse into subdirectories
for (Map.Entry<String, DirNode> child : node.childDirs.entrySet()) {
collectResources(child.getValue(), entryPrefix,
rlSubPath + child.getKey() + "/", zipFile, namespace, output);
}
}
}