Improve .hex font loading code.

- Optimize string logic to reduce unnecessary object churn (~2x faster font load times).
- Add support for multiple font.hex IResources - this allows resource packs to only partially
  override the OpenComputers font without removing existing glyphs, opening the door for
  non-compatibility-breaking glyph addition packs or partial alternate fonts.
This commit is contained in:
Adrian Siekierka 2022-11-11 10:13:02 +01:00
parent 707ffc57a5
commit dc55abe1c4
2 changed files with 75 additions and 47 deletions

View File

@ -6,6 +6,7 @@ import li.cil.oc.OpenComputers;
import li.cil.oc.Settings; import li.cil.oc.Settings;
import li.cil.oc.util.FontUtils; import li.cil.oc.util.FontUtils;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.IResource;
import net.minecraft.util.ResourceLocation; import net.minecraft.util.ResourceLocation;
import org.lwjgl.BufferUtils; import org.lwjgl.BufferUtils;
@ -14,6 +15,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List;
public class FontParserHex implements IGlyphProvider { public class FontParserHex implements IGlyphProvider {
private static final byte[] OPAQUE = {(byte) 255, (byte) 255, (byte) 255, (byte) 255}; private static final byte[] OPAQUE = {(byte) 255, (byte) 255, (byte) 255, (byte) 255};
@ -21,47 +23,68 @@ public class FontParserHex implements IGlyphProvider {
private final TIntObjectMap<byte[]> glyphs = new TIntObjectHashMap<>(); private final TIntObjectMap<byte[]> glyphs = new TIntObjectHashMap<>();
private static int hex2int(char c) {
if (c >= '0' && c <= '9') {
return c - '0';
} else if (c >= 'A' && c <= 'F') {
return c - ('A' - 10);
} else if (c >= 'a' && c <= 'f') {
return c - ('a' - 10);
} else {
throw new RuntimeException("invalid char: " + c);
}
}
@Override @Override
public void initialize() { public void initialize() {
try { try {
final InputStream font = Minecraft.getMinecraft().getResourceManager().getResource(new ResourceLocation(Settings.resourceDomain(), "font.hex")).getInputStream(); glyphs.clear();
try {
OpenComputers.log().info("Initializing unicode glyph provider."); OpenComputers.log().info("Loading Unicode glyphs...");
final BufferedReader input = new BufferedReader(new InputStreamReader(font)); long time = System.currentTimeMillis();
String line; int glyphCount = 0;
int glyphCount = 0;
while ((line = input.readLine()) != null) { ResourceLocation loc = new ResourceLocation(Settings.resourceDomain(), "font.hex");
final String[] info = line.split(":"); for (IResource resource : (List<IResource>) Minecraft.getMinecraft().getResourceManager().getAllResources(loc)) {
final int charCode = Integer.parseInt(info[0], 16); final InputStream font = resource.getInputStream();
if (charCode < 0 || charCode >= FontUtils.codepoint_limit()) {
OpenComputers.log().warn(String.format("Unicode font contained unexpected glyph: U+%04X, ignoring", charCode));
continue; // Out of bounds.
}
final int expectedWidth = FontUtils.wcwidth(charCode);
if (expectedWidth < 1) continue; // Skip control characters.
// Two chars representing one byte represent one row of eight pixels.
final byte[] glyph = new byte[info[1].length() >> 1];
final int glyphWidth = glyph.length / getGlyphHeight();
if (expectedWidth == glyphWidth) {
for (int i = 0; i < glyph.length; i++) {
glyph[i] = (byte) Integer.parseInt(info[1].substring(i * 2, i * 2 + 2), 16);
}
if (!glyphs.containsKey(charCode)) {
glyphCount++;
}
glyphs.put(charCode, glyph);
} else if (Settings.get().logHexFontErrors()) {
OpenComputers.log().warn(String.format("Size of glyph for code point U+%04X (%s) in font (%d) does not match expected width (%d), ignoring.", charCode, String.valueOf((char) charCode), glyphWidth, expectedWidth));
}
}
OpenComputers.log().info("Loaded " + glyphCount + " glyphs.");
} finally {
try { try {
font.close(); final BufferedReader input = new BufferedReader(new InputStreamReader(font));
} catch (IOException ex) { String line;
OpenComputers.log().warn("Error parsing font.", ex); while ((line = input.readLine()) != null) {
final String info = line.substring(0, line.indexOf(':'));
final int charCode = Integer.parseInt(info, 16);
if (charCode < 0 || charCode >= FontUtils.codepoint_limit()) {
OpenComputers.log().warn(String.format("Unicode font contained unexpected glyph: U+%04X, ignoring", charCode));
continue; // Out of bounds.
}
final int expectedWidth = FontUtils.wcwidth(charCode);
if (expectedWidth < 1) continue; // Skip control characters.
// Two chars representing one byte represent one row of eight pixels.
int glyphStrOfs = info.length() + 1;
final byte[] glyph = new byte[(line.length() - glyphStrOfs) >> 1];
final int glyphWidth = glyph.length / getGlyphHeight();
if (expectedWidth == glyphWidth) {
for (int i = 0; i < glyph.length; i++, glyphStrOfs += 2) {
glyph[i] = (byte) ((hex2int(line.charAt(glyphStrOfs)) << 4) | (hex2int(line.charAt(glyphStrOfs + 1))));
}
if (!glyphs.containsKey(charCode)) {
glyphCount++;
}
glyphs.put(charCode, glyph);
} else if (Settings.get().logHexFontErrors()) {
OpenComputers.log().warn(String.format("Size of glyph for code point U+%04X (%s) in font (%d) does not match expected width (%d), ignoring.", charCode, String.valueOf((char) charCode), glyphWidth, expectedWidth));
}
}
} finally {
try {
font.close();
} catch (IOException ex) {
OpenComputers.log().warn("Error parsing font.", ex);
}
} }
} }
OpenComputers.log().info("Loaded " + glyphCount + " glyphs in " + (System.currentTimeMillis() - time) + " milliseconds.");
} catch (IOException ex) { } catch (IOException ex) {
OpenComputers.log().warn("Failed loading glyphs.", ex); OpenComputers.log().warn("Failed loading glyphs.", ex);
} }
@ -72,7 +95,7 @@ public class FontParserHex implements IGlyphProvider {
if (!glyphs.containsKey(charCode)) if (!glyphs.containsKey(charCode))
return null; return null;
final byte[] glyph = glyphs.get(charCode); final byte[] glyph = glyphs.get(charCode);
if (glyph == null || glyph.length <= 0) if (glyph == null || glyph.length == 0)
return null; return null;
final ByteBuffer buffer = BufferUtils.createByteBuffer(glyph.length * getGlyphWidth() * 4); final ByteBuffer buffer = BufferUtils.createByteBuffer(glyph.length * getGlyphWidth() * 4);
for (byte aGlyph : glyph) { for (byte aGlyph : glyph) {

View File

@ -138,23 +138,28 @@ object FontUtils {
else if ((charCode == 0xe0001) || ((charCode - 0xe0020) < 0x5f) || ((charCode - 0xe0100) < 0xef)) 0 else if ((charCode == 0xe0001) || ((charCode - 0xe0020) < 0x5f) || ((charCode - 0xe0100) < 0xef)) 0
else 1 else 1
} }
OpenComputers.log.info("Initializing unicode wcwidth.") {
for (i <- 0 until codepoint_limit) { OpenComputers.log.info("Initializing font glyph width cache...")
if (c_wcwidth(i) == 2) val time = System.currentTimeMillis()
defined_double_wide += i for (i <- 0 until codepoint_limit) {
if (c_wcwidth(i) == 2)
defined_double_wide += i
}
OpenComputers.log.info("Initialized font glyph width cache in " + (System.currentTimeMillis() - time) + " milliseconds.")
} }
try { try {
OpenComputers.log.info("Initializing font glyph widths.") OpenComputers.log.info("Initializing font glyph width overrides...")
val time = System.currentTimeMillis()
val font = FontUtils.getClass.getResourceAsStream("/assets/opencomputers/font.hex") val font = FontUtils.getClass.getResourceAsStream("/assets/opencomputers/font.hex")
try { try {
var line: String = null var line: String = null
val input = new BufferedReader(new InputStreamReader(font, StandardCharsets.UTF_8)) val input = new BufferedReader(new InputStreamReader(font, StandardCharsets.UTF_8))
var out_of_range_glyph: Int = 0 var out_of_range_glyph: Int = 0
while ({line = input.readLine; line != null}) { while ({line = input.readLine; line != null}) {
val info = line.split(":") val info = line.substring(0, line.indexOf(':'))
val charCode = Integer.parseInt(info(0), 16) val charCode = Integer.parseInt(info, 16)
if (charCode >= 0 && charCode < codepoint_limit) { if (charCode >= 0 && charCode < codepoint_limit) {
info(1).trim.length match { line.length - info.length - 1 match {
case 64 => defined_double_wide += charCode case 64 => defined_double_wide += charCode
case 32 => defined_double_wide -= charCode case 32 => defined_double_wide -= charCode
case n => OpenComputers.log.warn(s"Invalid glyph size detected in font.hex. Expected 64 or 32, got: $n") case n => OpenComputers.log.warn(s"Invalid glyph size detected in font.hex. Expected 64 or 32, got: $n")
@ -163,8 +168,8 @@ object FontUtils {
out_of_range_glyph += 1 out_of_range_glyph += 1
} }
} }
if (out_of_range_glyph > 1) { if (out_of_range_glyph >= 1) {
OpenComputers.log.info(f"${out_of_range_glyph} total non-BMP glyph char codes detected in font.hex") OpenComputers.log.info(f"${out_of_range_glyph} total out-of-bounds glyph char codes detected in font.hex")
} }
} finally { } finally {
try { try {
@ -173,9 +178,9 @@ object FontUtils {
case ex: Throwable => OpenComputers.log.error(s"Error closing font.hex: $ex") case ex: Throwable => OpenComputers.log.error(s"Error closing font.hex: $ex")
} }
} }
OpenComputers.log.info("Initialized font glyph width overrides in " + (System.currentTimeMillis() - time) + " milliseconds.")
} catch { } catch {
case ex: Throwable => OpenComputers.log.error(s"Error parsing glyphs to determine widths: $ex") case ex: Throwable => OpenComputers.log.error(s"Error parsing glyphs to determine widths: $ex")
} }
OpenComputers.log.info("glyph width ready.")
} }
} }