diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/text/BuiltinFontFace.java b/src/main/java/meteordevelopment/meteorclient/renderer/text/BuiltinFontFace.java index 8ee420efa6..63c3e12177 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/BuiltinFontFace.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/BuiltinFontFace.java @@ -1,10 +1,14 @@ package meteordevelopment.meteorclient.renderer.text; import meteordevelopment.meteorclient.utils.render.FontUtils; +import org.jspecify.annotations.NullMarked; import java.io.InputStream; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; -public class BuiltinFontFace extends FontFace { +@NullMarked +public non-sealed class BuiltinFontFace extends FontFace { private final String name; public BuiltinFontFace(FontInfo info, String name) { @@ -14,10 +18,12 @@ public BuiltinFontFace(FontInfo info, String name) { } @Override - public InputStream toStream() { - InputStream in = FontUtils.stream(name); - if (in == null) throw new RuntimeException("Failed to load builtin font " + name + "."); - return in; + public ReadableByteChannel byteChannelForRead() { + InputStream inputStream = FontUtils.builtinFontStream(this.name); + if (inputStream == null) { + throw new IllegalArgumentException("Builtin font '" + this.name + "' not found"); + } + return Channels.newChannel(inputStream); } @Override diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/text/CustomTextRenderer.java b/src/main/java/meteordevelopment/meteorclient/renderer/text/CustomTextRenderer.java index c31faabc6e..02ffe511db 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/CustomTextRenderer.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/CustomTextRenderer.java @@ -8,11 +8,10 @@ import meteordevelopment.meteorclient.renderer.MeshBuilder; import meteordevelopment.meteorclient.renderer.MeshRenderer; import meteordevelopment.meteorclient.renderer.MeteorRenderPipelines; -import meteordevelopment.meteorclient.utils.Utils; import meteordevelopment.meteorclient.utils.render.color.Color; import net.minecraft.client.MinecraftClient; -import org.lwjgl.BufferUtils; +import java.io.IOException; import java.nio.ByteBuffer; public class CustomTextRenderer implements TextRenderer { @@ -30,11 +29,10 @@ public class CustomTextRenderer implements TextRenderer { private double fontScale = 1; private double scale = 1; - public CustomTextRenderer(FontFace fontFace) { + public CustomTextRenderer(FontFace fontFace) throws IOException { this.fontFace = fontFace; - byte[] bytes = Utils.readBytes(fontFace.toStream()); - ByteBuffer buffer = BufferUtils.createByteBuffer(bytes.length).put(bytes).flip(); + ByteBuffer buffer = fontFace.readToDirectByteBuffer(); fonts = new Font[5]; for (int i = 0; i < fonts.length; i++) { diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/text/FontFace.java b/src/main/java/meteordevelopment/meteorclient/renderer/text/FontFace.java index b46a706813..aa814e8275 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/FontFace.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/FontFace.java @@ -1,15 +1,28 @@ package meteordevelopment.meteorclient.renderer.text; -import java.io.InputStream; +import meteordevelopment.meteorclient.utils.files.ByteBufferUtils; +import org.jspecify.annotations.NullMarked; +import org.lwjgl.BufferUtils; -public abstract class FontFace { +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; + +@NullMarked +public abstract sealed class FontFace permits BuiltinFontFace, SystemFontFace { public final FontInfo info; protected FontFace(FontInfo info) { this.info = info; } - public abstract InputStream toStream(); + public abstract ReadableByteChannel byteChannelForRead() throws IOException; + + public final ByteBuffer readToDirectByteBuffer() throws IOException { + try (ReadableByteChannel channel = byteChannelForRead()) { + return ByteBufferUtils.readFully(channel, BufferUtils::createByteBuffer); + } + } @Override public String toString() { diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/text/SystemFontFace.java b/src/main/java/meteordevelopment/meteorclient/renderer/text/SystemFontFace.java index 7395d3d083..fc8c3868dd 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/SystemFontFace.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/SystemFontFace.java @@ -1,11 +1,15 @@ package meteordevelopment.meteorclient.renderer.text; -import meteordevelopment.meteorclient.utils.render.FontUtils; +import org.jspecify.annotations.NullMarked; -import java.io.InputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; -public class SystemFontFace extends FontFace { +@NullMarked +public final class SystemFontFace extends FontFace { private final Path path; public SystemFontFace(FontInfo info, Path path) { @@ -15,14 +19,8 @@ public SystemFontFace(FontInfo info, Path path) { } @Override - public InputStream toStream() { - if (!path.toFile().exists()) { - throw new RuntimeException("Tried to load font that no longer exists."); - } - - InputStream in = FontUtils.stream(path.toFile()); - if (in == null) throw new RuntimeException("Failed to load font from " + path + "."); - return in; + public ReadableByteChannel byteChannelForRead() throws IOException { + return FileChannel.open(this.path, StandardOpenOption.READ); } @Override diff --git a/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java b/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java index 3316565d67..9499c71ab1 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java @@ -27,8 +27,8 @@ import net.minecraft.util.Identifier; import org.joml.Quaternionf; import org.joml.Vector3f; -import org.lwjgl.BufferUtils; +import java.io.IOException; import java.nio.ByteBuffer; import java.time.Duration; import java.util.ArrayList; @@ -298,10 +298,12 @@ private void onCustomFontChanged(CustomFontChangedEvent event) { } private static FontHolder loadFont(int height) { - byte[] data = Utils.readBytes(Fonts.RENDERER.fontFace.toStream()); - ByteBuffer buffer = BufferUtils.createByteBuffer(data.length).put(data).flip(); - - return new FontHolder(new Font(buffer, height)); + try { + ByteBuffer buffer = Fonts.RENDERER.fontFace.readToDirectByteBuffer(); + return new FontHolder(new Font(buffer, height)); + } catch (IOException e) { + throw new RuntimeException("Failed to load font: " + Fonts.RENDERER.fontFace, e); + } } private static class FontHolder { diff --git a/src/main/java/meteordevelopment/meteorclient/utils/files/ByteBufferUtils.java b/src/main/java/meteordevelopment/meteorclient/utils/files/ByteBufferUtils.java new file mode 100644 index 0000000000..c5d8671ad7 --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/files/ByteBufferUtils.java @@ -0,0 +1,88 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.utils.files; + +import org.jspecify.annotations.NullMarked; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.function.IntFunction; + +import static java.nio.file.Files.*; + +@NullMarked +public final class ByteBufferUtils { + private ByteBufferUtils() {} + + public static ByteBuffer readFully(Path path, IntFunction allocator) throws IOException { + try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { + long size = size(path); + if (size > Integer.MAX_VALUE) { + throw new IOException("File too large to read into ByteBuffer: " + path); + } + ByteBuffer buffer = allocator.apply((int) size); + while (buffer.hasRemaining()) { + int bytesRead = channel.read(buffer); + if (bytesRead == -1) break; // EOF + } + buffer.flip(); + return buffer; + } + } + + public static ByteBuffer readFully(ReadableByteChannel channel, IntFunction allocator) throws IOException { + ByteBuffer buffer = requireCapacity(allocator.apply(8192), 8192); + + while (true) { + int bytesRead = channel.read(buffer); + + if (bytesRead == -1) break; + + if (bytesRead == 0) { + // Avoid busy-spin on non-blocking channels. + // If buffer is full, grow; otherwise caller should probably be using blocking I/O. + if (!buffer.hasRemaining()) { + buffer = grow(buffer, allocator); + continue; + } + // In a "readFully" API, returning early is usually better than spinning forever. + // Alternative: Thread.onSpinWait(); continue; + break; + } + + if (!buffer.hasRemaining()) { + buffer = grow(buffer, allocator); + } + } + + buffer.flip(); + return buffer; + } + + private static ByteBuffer grow(ByteBuffer buffer, IntFunction allocator) { + int oldCap = buffer.capacity(); + int newCap = oldCap << 1; + if (newCap <= 0) throw new OutOfMemoryError("Buffer too large (overflow): " + oldCap); + + ByteBuffer newBuffer = requireCapacity(allocator.apply(newCap), newCap); + buffer.flip(); + newBuffer.put(buffer); + return newBuffer; + } + + private static ByteBuffer requireCapacity(ByteBuffer buf, int minCap) { + if (buf.capacity() < minCap) { + throw new IllegalArgumentException("Allocator returned capacity " + buf.capacity() + " < " + minCap); + } + return buf; + } + +} diff --git a/src/main/java/meteordevelopment/meteorclient/utils/render/FontUtils.java b/src/main/java/meteordevelopment/meteorclient/utils/render/FontUtils.java index 6fe61ad6d4..6bd65d63d4 100644 --- a/src/main/java/meteordevelopment/meteorclient/utils/render/FontUtils.java +++ b/src/main/java/meteordevelopment/meteorclient/utils/render/FontUtils.java @@ -2,55 +2,86 @@ * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). * Copyright (c) Meteor Development. */ - package meteordevelopment.meteorclient.utils.render; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import meteordevelopment.meteorclient.MeteorClient; import meteordevelopment.meteorclient.renderer.Fonts; import meteordevelopment.meteorclient.renderer.text.*; -import meteordevelopment.meteorclient.utils.Utils; +import meteordevelopment.meteorclient.utils.files.ByteBufferUtils; import net.minecraft.util.Util; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.lwjgl.BufferUtils; import org.lwjgl.stb.STBTTFontinfo; import org.lwjgl.stb.STBTruetype; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.InputStream; import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; import java.nio.charset.StandardCharsets; -import java.util.HashSet; import java.util.List; import java.util.Set; -public class FontUtils { - private FontUtils() { +@NullMarked +public final class FontUtils { + private FontUtils() {} + + public static @Nullable FontInfo getSysFontInfo(File file) { + return getFontInfo(file); } - public static FontInfo getSysFontInfo(File file) { - return getFontInfo(stream(file)); + public static @Nullable FontInfo getBuiltinFontInfo(String builtin) { + return getFontInfo(builtinFontStream(builtin)); } - public static FontInfo getBuiltinFontInfo(String builtin) { - return getFontInfo(stream(builtin)); + /** + * System font path: avoid heap byte[] by reading the file into a direct buffer. + */ + private static @Nullable FontInfo getFontInfo(@Nullable File file) { + if (file == null || !file.isFile()) return null; + + try { + return getFontInfo(ByteBufferUtils.readFully(file.toPath(), BufferUtils::createByteBuffer)); + } catch (Exception e) { + MeteorClient.LOG.warn("Failed to read font file: {}", file, e); + return null; + } } - public static FontInfo getFontInfo(InputStream stream) { + /** + * Builtin/resource path: stream into a direct buffer (no byte[] intermediate). + */ + public static @Nullable FontInfo getFontInfo(@Nullable InputStream stream) { if (stream == null) return null; - byte[] bytes = Utils.readBytes(stream); - if (bytes.length < 5) return null; + try (ReadableByteChannel ch = Channels.newChannel(stream)) { + ByteBuffer buf = ByteBufferUtils.readFully(ch, BufferUtils::createByteBuffer); + return getFontInfo(buf); + } catch (Exception e) { + MeteorClient.LOG.warn("Failed to read font stream.", e); + return null; + } + } + + /** + * Core logic: interpret font data from a ByteBuffer. + * NOTE: This preserves your original 5-byte header check exactly. + */ + private static @Nullable FontInfo getFontInfo(ByteBuffer buffer) { + if (buffer.remaining() < 5) return null; + // Preserve existing check: 00 01 00 00 00 if ( - bytes[0] != 0 || - bytes[1] != 1 || - bytes[2] != 0 || - bytes[3] != 0 || - bytes[4] != 0 + buffer.get(0) != 0 || + buffer.get(1) != 1 || + buffer.get(2) != 0 || + buffer.get(3) != 0 || + buffer.get(4) != 0 ) return null; - ByteBuffer buffer = BufferUtils.createByteBuffer(bytes.length).put(bytes).flip(); STBTTFontinfo fontInfo = STBTTFontinfo.create(); if (!STBTruetype.stbtt_InitFont(fontInfo, buffer)) return null; @@ -65,7 +96,7 @@ public static FontInfo getFontInfo(InputStream stream) { } public static Set getSearchPaths() { - Set paths = new HashSet<>(); + Set paths = new ObjectOpenHashSet<>(); paths.add(System.getProperty("java.home") + "/lib/fonts"); for (File dir : getUFontDirs()) { @@ -136,7 +167,7 @@ public static void loadSystem(List fontList, File dir) { } } - public static boolean addFont(List fontList, FontFace font) { + private static boolean addFont(List fontList, @Nullable FontFace font) { if (font == null) return false; FontInfo info = font.info; @@ -152,17 +183,8 @@ public static boolean addFont(List fontList, FontFace font) { return family.addFont(font); } - public static InputStream stream(String builtin) { - return FontUtils.class.getResourceAsStream("/assets/" + MeteorClient.MOD_ID + "/fonts/" + builtin + ".ttf"); + public static @Nullable InputStream builtinFontStream(String name) { + return FontUtils.class.getResourceAsStream("/assets/" + MeteorClient.MOD_ID + "/fonts/" + name + ".ttf"); } - public static InputStream stream(File file) { - try { - return new FileInputStream(file); - } - catch (FileNotFoundException e) { - e.printStackTrace(); - return null; - } - } }