mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-10-26 14:24:30 +00:00
Fix crash when mods measure text off-thread with a custom font (#2204)
This commit is contained in:
1 parent
7cd97d1216
commit
61bb779dea
9 files changed
+540
-198
No files matched your search
@@ -308,6 +308,11 @@ public class GLStateManager {
|
|||||||
return Thread.currentThread() == MainThread;
|
return Thread.currentThread() == MainThread;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void requireContext(String call) {
|
||||||
|
if (Thread.currentThread() == MainThread || RENDER_BACKEND.hasContextOnThread()) return;
|
||||||
|
throw new IllegalStateException(call + " on thread " + Thread.currentThread().getName() + ", which has no GL context");
|
||||||
|
}
|
||||||
|
|
||||||
@Getter private static GLSMInitConfig initConfig;
|
@Getter private static GLSMInitConfig initConfig;
|
||||||
|
|
||||||
public static ContextCapabilities capabilities;
|
public static ContextCapabilities capabilities;
|
||||||
@@ -2525,11 +2530,13 @@ public class GLStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static int glGenTextures() {
|
public static int glGenTextures() {
|
||||||
|
requireContext("glGenTextures");
|
||||||
flushDeferredTextureDeletes();
|
flushDeferredTextureDeletes();
|
||||||
return RENDER_BACKEND.genTextures();
|
return RENDER_BACKEND.genTextures();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void glGenTextures(IntBuffer textures) {
|
public static void glGenTextures(IntBuffer textures) {
|
||||||
|
requireContext("glGenTextures");
|
||||||
flushDeferredTextureDeletes();
|
flushDeferredTextureDeletes();
|
||||||
RENDER_BACKEND.genTextures(textures);
|
RENDER_BACKEND.genTextures(textures);
|
||||||
}
|
}
|
||||||
@@ -5325,13 +5332,18 @@ public class GLStateManager {
|
|||||||
}
|
}
|
||||||
public static void glClearTexImage(int texture, int level, int format, int type, ByteBuffer data) { RENDER_BACKEND.clearTexImage(texture, level, format, type); }
|
public static void glClearTexImage(int texture, int level, int format, int type, ByteBuffer data) { RENDER_BACKEND.clearTexImage(texture, level, format, type); }
|
||||||
|
|
||||||
public static int glGenSamplers() { return RENDER_BACKEND.genSamplers(); }
|
public static int glGenSamplers() {
|
||||||
|
requireContext("glGenSamplers");
|
||||||
|
return RENDER_BACKEND.genSamplers();
|
||||||
|
}
|
||||||
|
|
||||||
public static void glGenSamplers(IntBuffer samplers) {
|
public static void glGenSamplers(IntBuffer samplers) {
|
||||||
|
requireContext("glGenSamplers");
|
||||||
for (int i = samplers.position(); i < samplers.limit(); i++) samplers.put(i, RENDER_BACKEND.genSamplers());
|
for (int i = samplers.position(); i < samplers.limit(); i++) samplers.put(i, RENDER_BACKEND.genSamplers());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void glGenSamplers(int[] samplers) {
|
public static void glGenSamplers(int[] samplers) {
|
||||||
|
requireContext("glGenSamplers");
|
||||||
for (int i = 0; i < samplers.length; i++) samplers[i] = RENDER_BACKEND.genSamplers();
|
for (int i = 0; i < samplers.length; i++) samplers[i] = RENDER_BACKEND.genSamplers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6748,26 +6760,32 @@ public class GLStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static int glGenBuffers() {
|
public static int glGenBuffers() {
|
||||||
|
requireContext("glGenBuffers");
|
||||||
return RENDER_BACKEND.genBuffers();
|
return RENDER_BACKEND.genBuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void glGenBuffers(IntBuffer buffers) {
|
public static void glGenBuffers(IntBuffer buffers) {
|
||||||
|
requireContext("glGenBuffers");
|
||||||
for (int i = buffers.position(); i < buffers.limit(); i++) buffers.put(i, RENDER_BACKEND.genBuffers());
|
for (int i = buffers.position(); i < buffers.limit(); i++) buffers.put(i, RENDER_BACKEND.genBuffers());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void glGenBuffers(int[] buffers) {
|
public static void glGenBuffers(int[] buffers) {
|
||||||
|
requireContext("glGenBuffers");
|
||||||
for (int i = 0; i < buffers.length; i++) buffers[i] = RENDER_BACKEND.genBuffers();
|
for (int i = 0; i < buffers.length; i++) buffers[i] = RENDER_BACKEND.genBuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int glCreateBuffers() {
|
public static int glCreateBuffers() {
|
||||||
|
requireContext("glCreateBuffers");
|
||||||
return RENDER_BACKEND.createBuffers();
|
return RENDER_BACKEND.createBuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void glCreateBuffers(IntBuffer buffers) {
|
public static void glCreateBuffers(IntBuffer buffers) {
|
||||||
|
requireContext("glCreateBuffers");
|
||||||
for (int i = buffers.position(); i < buffers.limit(); i++) buffers.put(i, RENDER_BACKEND.createBuffers());
|
for (int i = buffers.position(); i < buffers.limit(); i++) buffers.put(i, RENDER_BACKEND.createBuffers());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void glCreateBuffers(int[] buffers) {
|
public static void glCreateBuffers(int[] buffers) {
|
||||||
|
requireContext("glCreateBuffers");
|
||||||
for (int i = 0; i < buffers.length; i++) buffers[i] = RENDER_BACKEND.createBuffers();
|
for (int i = 0; i < buffers.length; i++) buffers[i] = RENDER_BACKEND.createBuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7158,6 +7176,7 @@ public class GLStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static int glGenVertexArrays() {
|
public static int glGenVertexArrays() {
|
||||||
|
requireContext("glGenVertexArrays");
|
||||||
return RENDER_BACKEND.genVertexArrays();
|
return RENDER_BACKEND.genVertexArrays();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7211,6 +7230,7 @@ public class GLStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static int glGenFramebuffers() {
|
public static int glGenFramebuffers() {
|
||||||
|
requireContext("glGenFramebuffers");
|
||||||
return RENDER_BACKEND.genFramebuffers();
|
return RENDER_BACKEND.genFramebuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7228,15 +7248,24 @@ public class GLStateManager {
|
|||||||
|
|
||||||
public static void glFramebufferTexture(int target, int attachment, int texture, int level) { RENDER_BACKEND.framebufferTexture(target, attachment, texture, level); }
|
public static void glFramebufferTexture(int target, int attachment, int texture, int level) { RENDER_BACKEND.framebufferTexture(target, attachment, texture, level); }
|
||||||
|
|
||||||
public static int glGenRenderbuffers() { return RENDER_BACKEND.genRenderbuffers(); }
|
public static int glGenRenderbuffers() {
|
||||||
|
requireContext("glGenRenderbuffers");
|
||||||
|
return RENDER_BACKEND.genRenderbuffers();
|
||||||
|
}
|
||||||
public static void glDeleteRenderbuffers(int renderbuffer) { RENDER_BACKEND.deleteRenderbuffers(renderbuffer); }
|
public static void glDeleteRenderbuffers(int renderbuffer) { RENDER_BACKEND.deleteRenderbuffers(renderbuffer); }
|
||||||
public static void glBindRenderbuffer(int target, int renderbuffer) { RENDER_BACKEND.bindRenderbuffer(target, renderbuffer); }
|
public static void glBindRenderbuffer(int target, int renderbuffer) { RENDER_BACKEND.bindRenderbuffer(target, renderbuffer); }
|
||||||
public static void glRenderbufferStorage(int target, int internalformat, int width, int height) { RENDER_BACKEND.renderbufferStorage(target, internalformat, width, height); }
|
public static void glRenderbufferStorage(int target, int internalformat, int width, int height) { RENDER_BACKEND.renderbufferStorage(target, internalformat, width, height); }
|
||||||
public static void glRenderbufferStorageMultisample(int target, int samples, int internalformat, int width, int height) { RENDER_BACKEND.renderbufferStorageMultisample(target, samples, internalformat, width, height); }
|
public static void glRenderbufferStorageMultisample(int target, int samples, int internalformat, int width, int height) { RENDER_BACKEND.renderbufferStorageMultisample(target, samples, internalformat, width, height); }
|
||||||
public static void glFramebufferRenderbuffer(int target, int attachment, int renderbuffertarget, int renderbuffer) { RENDER_BACKEND.framebufferRenderbuffer(target, attachment, renderbuffertarget, renderbuffer); }
|
public static void glFramebufferRenderbuffer(int target, int attachment, int renderbuffertarget, int renderbuffer) { RENDER_BACKEND.framebufferRenderbuffer(target, attachment, renderbuffertarget, renderbuffer); }
|
||||||
|
|
||||||
public static void glGenQueries(IntBuffer ids) { RENDER_BACKEND.genQueries(ids); }
|
public static void glGenQueries(IntBuffer ids) {
|
||||||
public static int glGenQueries() { return RENDER_BACKEND.genQueries(); }
|
requireContext("glGenQueries");
|
||||||
|
RENDER_BACKEND.genQueries(ids);
|
||||||
|
}
|
||||||
|
public static int glGenQueries() {
|
||||||
|
requireContext("glGenQueries");
|
||||||
|
return RENDER_BACKEND.genQueries();
|
||||||
|
}
|
||||||
public static void glDeleteQueries(int id) { RENDER_BACKEND.deleteQueries(id); }
|
public static void glDeleteQueries(int id) { RENDER_BACKEND.deleteQueries(id); }
|
||||||
public static void glBeginQuery(int target, int id) { RENDER_BACKEND.beginQuery(target, id); }
|
public static void glBeginQuery(int target, int id) { RENDER_BACKEND.beginQuery(target, id); }
|
||||||
public static void glEndQuery(int target) { RENDER_BACKEND.endQuery(target); }
|
public static void glEndQuery(int target) { RENDER_BACKEND.endQuery(target); }
|
||||||
@@ -7916,6 +7945,7 @@ public class GLStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static int glCreateShader(int type) {
|
public static int glCreateShader(int type) {
|
||||||
|
requireContext("glCreateShader");
|
||||||
return RENDER_BACKEND.createShader(type);
|
return RENDER_BACKEND.createShader(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7924,6 +7954,7 @@ public class GLStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static int glCreateProgram() {
|
public static int glCreateProgram() {
|
||||||
|
requireContext("glCreateProgram");
|
||||||
final int program = RENDER_BACKEND.createProgram();
|
final int program = RENDER_BACKEND.createProgram();
|
||||||
programsPendingDeletion.remove(program);
|
programsPendingDeletion.remove(program);
|
||||||
programLifetimeGeneration++;
|
programLifetimeGeneration++;
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public abstract class RenderBackend {
|
|||||||
/** Returns true if the current thread has a valid render context. */
|
/** Returns true if the current thread has a valid render context. */
|
||||||
public abstract boolean hasContext();
|
public abstract boolean hasContext();
|
||||||
|
|
||||||
|
/** Returns true if the calling thread may issue render commands. */
|
||||||
|
public boolean hasContextOnThread() { return hasContext(); }
|
||||||
|
|
||||||
/** Returns true if the calling thread owns the "GL Context" */
|
/** Returns true if the calling thread owns the "GL Context" */
|
||||||
public abstract boolean isCurrent();
|
public abstract boolean isCurrent();
|
||||||
|
|
||||||
|
|||||||
@@ -133,11 +133,13 @@ public class DSAARB extends DSAUnsupported {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int createFramebuffer() {
|
public int createFramebuffer() {
|
||||||
|
GLStateManager.requireContext("glCreateFramebuffers");
|
||||||
return RENDER_BACKEND.createFramebuffers();
|
return RENDER_BACKEND.createFramebuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int createTexture(int target) {
|
public int createTexture(int target) {
|
||||||
|
GLStateManager.requireContext("glCreateTextures");
|
||||||
return RENDER_BACKEND.createTextures(target);
|
return RENDER_BACKEND.createTextures(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ public class DSAUnsupported implements DSAAccess {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int createTexture(int target) {
|
public int createTexture(int target) {
|
||||||
final int texture = RENDER_BACKEND.genTextures();
|
final int texture = GLStateManager.glGenTextures();
|
||||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
|
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
|
||||||
return texture;
|
return texture;
|
||||||
}
|
}
|
||||||
|
|||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
package com.gtnewhorizons.angelica.glsm;
|
||||||
|
|
||||||
|
import com.gtnewhorizons.angelica.glsm.testutil.TestThreads;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.function.Executable;
|
||||||
|
import org.lwjgl.BufferUtils;
|
||||||
|
import org.lwjgl.opengl.Display;
|
||||||
|
import org.lwjgl.opengl.GL11;
|
||||||
|
import org.lwjgl.opengl.GL20;
|
||||||
|
import org.lwjgl.opengl.SharedDrawable;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
@GLCoreTest
|
||||||
|
public class GLSM_OffContextCreation_GLTest {
|
||||||
|
|
||||||
|
private SharedDrawable sharedDrawable;
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanup() {
|
||||||
|
if (sharedDrawable != null) {
|
||||||
|
DrawableContexts.destroy(sharedDrawable);
|
||||||
|
sharedDrawable = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Executable> creationCalls() {
|
||||||
|
final Map<String, Executable> calls = new LinkedHashMap<>();
|
||||||
|
calls.put("glGenTextures()", GLStateManager::glGenTextures);
|
||||||
|
calls.put("glGenTextures(IntBuffer)", () -> GLStateManager.glGenTextures(BufferUtils.createIntBuffer(1)));
|
||||||
|
calls.put("glGenSamplers()", GLStateManager::glGenSamplers);
|
||||||
|
calls.put("glGenSamplers(IntBuffer)", () -> GLStateManager.glGenSamplers(BufferUtils.createIntBuffer(1)));
|
||||||
|
calls.put("glGenSamplers(int[])", () -> GLStateManager.glGenSamplers(new int[1]));
|
||||||
|
calls.put("glGenBuffers()", GLStateManager::glGenBuffers);
|
||||||
|
calls.put("glGenBuffers(IntBuffer)", () -> GLStateManager.glGenBuffers(BufferUtils.createIntBuffer(1)));
|
||||||
|
calls.put("glGenBuffers(int[])", () -> GLStateManager.glGenBuffers(new int[1]));
|
||||||
|
calls.put("glCreateBuffers()", GLStateManager::glCreateBuffers);
|
||||||
|
calls.put("glCreateBuffers(IntBuffer)", () -> GLStateManager.glCreateBuffers(BufferUtils.createIntBuffer(1)));
|
||||||
|
calls.put("glCreateBuffers(int[])", () -> GLStateManager.glCreateBuffers(new int[1]));
|
||||||
|
calls.put("glGenVertexArrays()", GLStateManager::glGenVertexArrays);
|
||||||
|
calls.put("glGenFramebuffers()", GLStateManager::glGenFramebuffers);
|
||||||
|
calls.put("glGenRenderbuffers()", GLStateManager::glGenRenderbuffers);
|
||||||
|
calls.put("glGenQueries()", GLStateManager::glGenQueries);
|
||||||
|
calls.put("glGenQueries(IntBuffer)", () -> GLStateManager.glGenQueries(BufferUtils.createIntBuffer(1)));
|
||||||
|
calls.put("glCreateShader", () -> GLStateManager.glCreateShader(GL20.GL_VERTEX_SHADER));
|
||||||
|
calls.put("glCreateProgram", GLStateManager::glCreateProgram);
|
||||||
|
calls.put("RenderSystem.createTexture", () -> RenderSystem.createTexture(GL11.GL_TEXTURE_2D));
|
||||||
|
calls.put("RenderSystem.createFramebuffer", RenderSystem::createFramebuffer);
|
||||||
|
return calls;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void creationWithoutContextThrows() throws InterruptedException {
|
||||||
|
TestThreads.run("glsm-no-context", () -> {
|
||||||
|
for (Map.Entry<String, Executable> call : creationCalls().entrySet()) {
|
||||||
|
assertThrows(IllegalStateException.class, call.getValue(), call.getKey() + " did not throw off-context");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void creationOnSharedContextThreadPasses() throws Exception {
|
||||||
|
sharedDrawable = new SharedDrawable(Display.getDrawable());
|
||||||
|
DrawableContexts.run(sharedDrawable, "glsm-shared-context", () -> {
|
||||||
|
final int texture = GLStateManager.glGenTextures();
|
||||||
|
assertNotEquals(0, texture, "no texture name on a thread that owns a shared context");
|
||||||
|
GLStateManager.glDeleteTextures(texture);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -543,6 +543,10 @@ public class SDLGPURenderBackend extends RenderBackend {
|
|||||||
return device.getDevice() != 0;
|
return device.getDevice() != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override public boolean hasContextOnThread() {
|
||||||
|
return hasContext() && (Thread.currentThread() == GLStateManager.getMainThread() || tlState.get() != null);
|
||||||
|
}
|
||||||
|
|
||||||
@Override public boolean isCurrent() {
|
@Override public boolean isCurrent() {
|
||||||
return Thread.currentThread() == GLStateManager.getMainThread();
|
return Thread.currentThread() == GLStateManager.getMainThread();
|
||||||
}
|
}
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.gtnewhorizons.angelica.sdlgpu.glsm;
|
||||||
|
|
||||||
|
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||||
|
import com.gtnewhorizons.angelica.glsm.testutil.TestThreads;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
@Tag("glsm-sdl")
|
||||||
|
@ExtendWith(GlsmSdlFrameExtension.class)
|
||||||
|
class GlsmSdlOffContextCreationTest {
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void boot() {
|
||||||
|
GlsmSdlHeadlessRig.boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void creationWithoutContextThrows() throws InterruptedException {
|
||||||
|
TestThreads.run("sdl-no-context", () -> assertThrows(IllegalStateException.class, GLStateManager::glGenTextures));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void creationOnRenderThreadPasses() {
|
||||||
|
final int texture = GLStateManager.glGenTextures();
|
||||||
|
assertNotEquals(0, texture);
|
||||||
|
GLStateManager.glDeleteTextures(texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
package com.gtnewhorizons.angelica.client.font;
|
package com.gtnewhorizons.angelica.client.font;
|
||||||
|
|
||||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
|
||||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAllocInt;
|
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAllocInt;
|
||||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
|
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
|
||||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
|
|
||||||
|
|
||||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||||
import jss.util.RandomXoshiro256StarStar;
|
import jss.util.RandomXoshiro256StarStar;
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.Value;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.lwjgl.opengl.GL11;
|
import org.lwjgl.opengl.GL11;
|
||||||
@@ -20,10 +16,9 @@ import java.awt.FontMetrics;
|
|||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
import java.awt.RenderingHints;
|
import java.awt.RenderingHints;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.ColorModel;
|
|
||||||
import java.awt.image.DataBufferInt;
|
import java.awt.image.DataBufferInt;
|
||||||
import java.awt.image.Raster;
|
|
||||||
import java.nio.IntBuffer;
|
import java.nio.IntBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import static com.gtnewhorizons.angelica.client.font.FontStrategist.getFontName;
|
import static com.gtnewhorizons.angelica.client.font.FontStrategist.getFontName;
|
||||||
@@ -33,222 +28,241 @@ public final class FontProviderCustom implements FontProvider {
|
|||||||
public static final Logger LOGGER = LogManager.getLogger("Angelica");
|
public static final Logger LOGGER = LogManager.getLogger("Angelica");
|
||||||
static final int ATLAS_SIZE = 128;
|
static final int ATLAS_SIZE = 128;
|
||||||
static final int ATLAS_COUNT = 512;
|
static final int ATLAS_COUNT = 512;
|
||||||
private final byte id; // 0 - primary font, 1 - fallback font
|
|
||||||
private final RandomXoshiro256StarStar fontRandom = new RandomXoshiro256StarStar();
|
private final RandomXoshiro256StarStar fontRandom = new RandomXoshiro256StarStar();
|
||||||
private FontAtlas[] fontAtlases = new FontAtlas[ATLAS_COUNT];
|
private final FontAtlas[] fontAtlases = new FontAtlas[ATLAS_COUNT];
|
||||||
private float currentFontQuality = FontConfig.customFontQuality;
|
private final int[] atlasTextures = new int[ATLAS_COUNT];
|
||||||
@Setter
|
|
||||||
private Font font;
|
private Font font;
|
||||||
|
|
||||||
private FontProviderCustom(byte id) {
|
FontProviderCustom(Font font) {
|
||||||
this.id = id;
|
this.font = font;
|
||||||
Font[] availableFonts = FontStrategist.getAvailableFonts();
|
}
|
||||||
String myFontName = switch (this.id) {
|
|
||||||
case 0 -> FontConfig.customFontNamePrimary;
|
private static Font configured(String fontName) {
|
||||||
case 1 -> FontConfig.customFontNameFallback;
|
if (Objects.equals(fontName, "(none)")) {
|
||||||
default -> null;
|
return null;
|
||||||
};
|
|
||||||
if (Objects.equals(myFontName, "(none)")) {
|
|
||||||
this.font = null;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
int fontPos = -1;
|
for (Font available : FontStrategist.getAvailableFonts()) {
|
||||||
for (int i = 0; i < availableFonts.length; i++) {
|
if (Objects.equals(fontName, getFontName(available))) {
|
||||||
if (Objects.equals(myFontName, getFontName(availableFonts[i]))) {
|
return available.deriveFont((float) FontConfig.customFontQuality);
|
||||||
fontPos = i;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (fontPos == -1) {
|
LOGGER.info("Could not find previously set font \"{}\". ", fontName);
|
||||||
LOGGER.info("Could not find previously set font \"{}\". ", myFontName);
|
return null;
|
||||||
this.font = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.font = availableFonts[fontPos].deriveFont(this.currentFontQuality);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class InstLoader {
|
private static class InstLoader {
|
||||||
static final FontProviderCustom instance0 = new FontProviderCustom((byte)0);
|
static final FontProviderCustom instance0 = new FontProviderCustom(configured(FontConfig.customFontNamePrimary));
|
||||||
static final FontProviderCustom instance1 = new FontProviderCustom((byte)1);
|
static final FontProviderCustom instance1 = new FontProviderCustom(configured(FontConfig.customFontNameFallback));
|
||||||
}
|
}
|
||||||
public static FontProviderCustom getPrimary() { return InstLoader.instance0; }
|
public static FontProviderCustom getPrimary() { return InstLoader.instance0; }
|
||||||
public static FontProviderCustom getFallback() { return InstLoader.instance1; }
|
public static FontProviderCustom getFallback() { return InstLoader.instance1; }
|
||||||
|
|
||||||
|
public void setFont(Font font) {
|
||||||
|
synchronized (this) {
|
||||||
|
this.font = font;
|
||||||
|
Arrays.fill(this.fontAtlases, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < ATLAS_COUNT; i++) {
|
||||||
|
if (this.atlasTextures[i] != 0) {
|
||||||
|
GLStateManager.glDeleteTextures(this.atlasTextures[i]);
|
||||||
|
this.atlasTextures[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void reloadFont(int fontID) {
|
public void reloadFont(int fontID) {
|
||||||
this.currentFontQuality = FontConfig.customFontQuality;
|
setFont(FontStrategist.getAvailableFonts()[fontID].deriveFont((float) FontConfig.customFontQuality));
|
||||||
this.font = FontStrategist.getAvailableFonts()[fontID].deriveFont(this.currentFontQuality);
|
|
||||||
|
|
||||||
for (FontAtlas atlas : fontAtlases) {
|
|
||||||
if (atlas != null && atlas.texture != 0) {
|
|
||||||
GLStateManager.glDeleteTextures(atlas.texture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fontAtlases = new FontAtlas[ATLAS_COUNT];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Value
|
private static final class FontAtlas {
|
||||||
private class GlyphData {
|
static final int STRIDE = 6;
|
||||||
float uStart;
|
static final int U_START = 0;
|
||||||
float vStart;
|
static final int V_START = 1;
|
||||||
float xAdvance;
|
static final int X_ADVANCE = 2;
|
||||||
float glyphW;
|
static final int GLYPH_W = 3;
|
||||||
float uSz;
|
static final int U_SIZE = 4;
|
||||||
float vSz;
|
static final int V_SIZE = 5;
|
||||||
}
|
|
||||||
|
|
||||||
private class FontAtlas {
|
final float[] metrics;
|
||||||
GlyphData[] glyphData = new GlyphData[ATLAS_SIZE];
|
final long availLo;
|
||||||
private int texture;
|
final long availHi;
|
||||||
private final int id;
|
|
||||||
|
|
||||||
FontAtlas(int id) {
|
FontAtlas(float[] metrics, long availLo, long availHi) {
|
||||||
this.id = id;
|
this.metrics = metrics;
|
||||||
|
this.availLo = availLo;
|
||||||
|
this.availHi = availHi;
|
||||||
}
|
}
|
||||||
|
|
||||||
void construct(Font font) {
|
boolean has(int slot) {
|
||||||
int atlasChars = 0;
|
return bit(this.availLo, this.availHi, slot);
|
||||||
for (int i = 0; i < ATLAS_SIZE; i++) {
|
}
|
||||||
final char ch = (char) (i + ATLAS_SIZE * this.id);
|
|
||||||
if (font.canDisplay(ch)) { atlasChars++; }
|
|
||||||
}
|
|
||||||
if (atlasChars == 0) { return; }
|
|
||||||
|
|
||||||
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
|
static boolean bit(long lo, long hi, int slot) {
|
||||||
Graphics2D g2d = image.createGraphics();
|
return ((slot < 64 ? lo >>> slot : hi >>> (slot - 64)) & 1L) != 0L;
|
||||||
g2d.setFont(font);
|
}
|
||||||
FontMetrics fm = g2d.getFontMetrics();
|
}
|
||||||
|
|
||||||
|
private static final FontAtlas EMPTY = new FontAtlas(new float[ATLAS_SIZE * FontAtlas.STRIDE], 0L, 0L);
|
||||||
|
|
||||||
|
private static void setHints(Graphics2D g2d) {
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BufferedImage layout(Font font, int id, long availLo, long availHi, float[] metricsOut) {
|
||||||
|
final float quality = font.getSize2D();
|
||||||
|
int atlasChars = Long.bitCount(availLo) + Long.bitCount(availHi);
|
||||||
|
|
||||||
|
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D g2d = image.createGraphics();
|
||||||
|
g2d.setFont(font);
|
||||||
|
FontMetrics fm = g2d.getFontMetrics();
|
||||||
|
|
||||||
|
final int atlasTilesX = (int) Math.ceil(Math.sqrt(atlasChars) * 1.5f);
|
||||||
|
final int atlasTilesY = (int) Math.ceil((double) atlasChars / atlasTilesX);
|
||||||
|
final float charSeparator = quality / 3f;
|
||||||
|
int rowWidth = 0;
|
||||||
|
int maxRowWidth = 0;
|
||||||
|
atlasChars = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < ATLAS_SIZE; i++) {
|
||||||
|
if (atlasChars % atlasTilesX == 0) {
|
||||||
|
maxRowWidth = Math.max(maxRowWidth, rowWidth);
|
||||||
|
rowWidth = 0;
|
||||||
|
}
|
||||||
|
if (FontAtlas.bit(availLo, availHi, i)) {
|
||||||
|
rowWidth += (int) (charSeparator + fm.charWidth((char) (i + ATLAS_SIZE * id)));
|
||||||
|
atlasChars++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maxRowWidth = Math.max(maxRowWidth, rowWidth);
|
||||||
|
|
||||||
|
final int lineHeight = fm.getHeight();
|
||||||
|
final float desc = fm.getDescent();
|
||||||
|
|
||||||
|
final int imageWidth = (int) (maxRowWidth + charSeparator);
|
||||||
|
final int imageHeight = (int) ((charSeparator + lineHeight) * atlasTilesY + charSeparator);
|
||||||
|
|
||||||
|
final boolean rasterize = metricsOut == null;
|
||||||
|
if (rasterize) {
|
||||||
g2d.dispose();
|
g2d.dispose();
|
||||||
|
|
||||||
final int atlasTilesX = (int) Math.ceil(Math.sqrt(atlasChars) * 1.5f);
|
|
||||||
final int atlasTilesY = (int) Math.ceil((double) atlasChars / atlasTilesX);
|
|
||||||
final float charSeparator = currentFontQuality / 3f;
|
|
||||||
int rowWidth = 0;
|
|
||||||
int maxRowWidth = 0;
|
|
||||||
atlasChars = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < ATLAS_SIZE; i++) {
|
|
||||||
if (atlasChars % atlasTilesX == 0) {
|
|
||||||
maxRowWidth = Math.max(maxRowWidth, rowWidth);
|
|
||||||
rowWidth = 0;
|
|
||||||
}
|
|
||||||
final char ch = (char)(i + ATLAS_SIZE * this.id);
|
|
||||||
if (font.canDisplay(ch)) {
|
|
||||||
rowWidth += (int) (charSeparator + fm.charWidth(ch));
|
|
||||||
atlasChars++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
maxRowWidth = Math.max(maxRowWidth, rowWidth);
|
|
||||||
|
|
||||||
final int lineHeight = fm.getHeight();
|
|
||||||
final float desc = fm.getDescent();
|
|
||||||
|
|
||||||
final int imageWidth = (int) (maxRowWidth + charSeparator);
|
|
||||||
final int imageHeight = (int) ((charSeparator + lineHeight) * atlasTilesY + charSeparator);
|
|
||||||
|
|
||||||
image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
|
image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
|
||||||
g2d = image.createGraphics();
|
g2d = image.createGraphics();
|
||||||
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
setHints(g2d);
|
||||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
||||||
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
|
||||||
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
|
|
||||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
|
||||||
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
|
||||||
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
|
||||||
g2d.setFont(font);
|
g2d.setFont(font);
|
||||||
fm = g2d.getFontMetrics();
|
} else {
|
||||||
|
setHints(g2d);
|
||||||
int tileX = 0, tileY = 0; // position in atlas tiles
|
|
||||||
int imgX = (int) charSeparator; // position in pixels
|
|
||||||
|
|
||||||
for (int i = 0; i < ATLAS_SIZE; i++) {
|
|
||||||
final char ch = (char) (i + ATLAS_SIZE * this.id);
|
|
||||||
if (!font.canDisplay(ch)) { continue; }
|
|
||||||
|
|
||||||
if (tileX >= atlasTilesX) {
|
|
||||||
tileX = 0;
|
|
||||||
imgX = (int) charSeparator;
|
|
||||||
tileY++;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int charWidth = fm.charWidth(ch);
|
|
||||||
final float charAspectRatio = (float) charWidth / lineHeight;
|
|
||||||
final float inset = currentFontQuality / 16;
|
|
||||||
g2d.drawString(Character.toString(ch), imgX, (lineHeight + charSeparator) * (tileY + 1) - desc);
|
|
||||||
final float uStart = (float) (imgX - inset * charAspectRatio) / imageWidth;
|
|
||||||
final float vStart = ((lineHeight + charSeparator) * (tileY + 1) - lineHeight - inset) / imageHeight;
|
|
||||||
final float xAdvance = charAspectRatio * 8 * charWidth / (charWidth + 2 * inset * charAspectRatio);
|
|
||||||
final float glyphW = charAspectRatio * 8 + 1;
|
|
||||||
final float uSz = (float) (charWidth + 2 * inset * charAspectRatio) / imageWidth;
|
|
||||||
final float vSz = (float) (lineHeight + 2 * inset) / imageHeight;
|
|
||||||
this.glyphData[i] = new GlyphData(uStart, vStart, xAdvance, glyphW, uSz, vSz);
|
|
||||||
imgX += (int) (charWidth + charSeparator);
|
|
||||||
tileX++;
|
|
||||||
}
|
|
||||||
g2d.dispose();
|
|
||||||
|
|
||||||
int id = GLStateManager.glGenTextures();
|
|
||||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, id);
|
|
||||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
|
|
||||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
|
|
||||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
|
|
||||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);
|
|
||||||
|
|
||||||
final int width = image.getWidth();
|
|
||||||
final int height = image.getHeight();
|
|
||||||
final IntBuffer pixelBuffer = memAllocInt(width * height);
|
|
||||||
long ptr = memAddress0(pixelBuffer);
|
|
||||||
|
|
||||||
if (image.getRaster().getDataBuffer() instanceof DataBufferInt dataBufferInt) {
|
|
||||||
final int[] pixelValues = dataBufferInt.getData();
|
|
||||||
|
|
||||||
for (int i : pixelValues) {
|
|
||||||
memPutInt(ptr, i);
|
|
||||||
ptr += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
pixelBuffer.limit(pixelValues.length);
|
|
||||||
} else {
|
|
||||||
Raster raster = image.getRaster();
|
|
||||||
ColorModel colorModel = image.getColorModel();
|
|
||||||
|
|
||||||
for (int y = 0; y < height; y++) {
|
|
||||||
for (int x = 0; x < width; x++) {
|
|
||||||
Object pixel = raster.getDataElements(x, y, null);
|
|
||||||
memPutInt(ptr, colorModel.getRGB(pixel));
|
|
||||||
ptr += 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pixelBuffer.limit(image.getHeight() * image.getWidth());
|
|
||||||
}
|
|
||||||
|
|
||||||
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, image.getWidth(), image.getHeight(), 0, GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, pixelBuffer);
|
|
||||||
memFree(pixelBuffer);
|
|
||||||
|
|
||||||
this.texture = id;
|
|
||||||
}
|
}
|
||||||
|
fm = g2d.getFontMetrics();
|
||||||
|
|
||||||
|
int tileX = 0, tileY = 0; // position in atlas tiles
|
||||||
|
int imgX = (int) charSeparator; // position in pixels
|
||||||
|
|
||||||
|
for (int i = 0; i < ATLAS_SIZE; i++) {
|
||||||
|
if (!FontAtlas.bit(availLo, availHi, i)) { continue; }
|
||||||
|
final char ch = (char) (i + ATLAS_SIZE * id);
|
||||||
|
|
||||||
|
if (tileX >= atlasTilesX) {
|
||||||
|
tileX = 0;
|
||||||
|
imgX = (int) charSeparator;
|
||||||
|
tileY++;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int charWidth = fm.charWidth(ch);
|
||||||
|
if (rasterize) {
|
||||||
|
g2d.drawString(Character.toString(ch), imgX, (lineHeight + charSeparator) * (tileY + 1) - desc);
|
||||||
|
} else {
|
||||||
|
final float charAspectRatio = (float) charWidth / lineHeight;
|
||||||
|
final float inset = quality / 16;
|
||||||
|
final int o = i * FontAtlas.STRIDE;
|
||||||
|
metricsOut[o + FontAtlas.U_START] = (float) (imgX - inset * charAspectRatio) / imageWidth;
|
||||||
|
metricsOut[o + FontAtlas.V_START] = ((lineHeight + charSeparator) * (tileY + 1) - lineHeight - inset) / imageHeight;
|
||||||
|
metricsOut[o + FontAtlas.X_ADVANCE] = charAspectRatio * 8 * charWidth / (charWidth + 2 * inset * charAspectRatio);
|
||||||
|
metricsOut[o + FontAtlas.GLYPH_W] = charAspectRatio * 8 + 1;
|
||||||
|
metricsOut[o + FontAtlas.U_SIZE] = (float) (charWidth + 2 * inset * charAspectRatio) / imageWidth;
|
||||||
|
metricsOut[o + FontAtlas.V_SIZE] = (float) (lineHeight + 2 * inset) / imageHeight;
|
||||||
|
}
|
||||||
|
imgX += (int) (charWidth + charSeparator);
|
||||||
|
tileX++;
|
||||||
|
}
|
||||||
|
g2d.dispose();
|
||||||
|
|
||||||
|
return rasterize ? image : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private FontAtlas getAtlas(char chr) {
|
private FontAtlas getAtlas(char chr) {
|
||||||
int id = chr / ATLAS_SIZE;
|
final FontAtlas fa = this.fontAtlases[chr / ATLAS_SIZE];
|
||||||
|
return fa != null ? fa : buildAtlas(chr / ATLAS_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized FontAtlas buildAtlas(int id) {
|
||||||
FontAtlas fa = this.fontAtlases[id];
|
FontAtlas fa = this.fontAtlases[id];
|
||||||
if (fa == null) {
|
if (fa != null) { return fa; }
|
||||||
fa = new FontAtlas(id);
|
final Font f = this.font;
|
||||||
fa.construct(this.font);
|
if (f == null) { return EMPTY; }
|
||||||
this.fontAtlases[id] = fa;
|
long availLo = 0L;
|
||||||
|
long availHi = 0L;
|
||||||
|
for (int i = 0; i < ATLAS_SIZE; i++) {
|
||||||
|
if (f.canDisplay((char) (i + ATLAS_SIZE * id))) {
|
||||||
|
if (i < 64) { availLo |= 1L << i; } else { availHi |= 1L << (i - 64); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if ((availLo | availHi) == 0L) {
|
||||||
|
fa = EMPTY;
|
||||||
|
} else {
|
||||||
|
final float[] metrics = new float[ATLAS_SIZE * FontAtlas.STRIDE];
|
||||||
|
layout(f, id, availLo, availHi, metrics);
|
||||||
|
fa = new FontAtlas(metrics, availLo, availHi);
|
||||||
|
}
|
||||||
|
this.fontAtlases[id] = fa;
|
||||||
return fa;
|
return fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private synchronized BufferedImage rasterize(int id) {
|
||||||
|
final Font f = this.font;
|
||||||
|
final FontAtlas fa = this.fontAtlases[id];
|
||||||
|
if (f == null || fa == null || fa == EMPTY) { return null; }
|
||||||
|
return layout(f, id, fa.availLo, fa.availHi, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int uploadAtlas(int id) {
|
||||||
|
final BufferedImage image = rasterize(id);
|
||||||
|
if (image == null) { return 0; }
|
||||||
|
|
||||||
|
final int texture = GLStateManager.glGenTextures();
|
||||||
|
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
|
||||||
|
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
|
||||||
|
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
|
||||||
|
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
|
||||||
|
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);
|
||||||
|
|
||||||
|
final int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
|
||||||
|
final IntBuffer pixelBuffer = memAllocInt(pixels.length);
|
||||||
|
pixelBuffer.put(pixels);
|
||||||
|
pixelBuffer.flip();
|
||||||
|
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, image.getWidth(), image.getHeight(), 0, GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, pixelBuffer);
|
||||||
|
memFree(pixelBuffer);
|
||||||
|
|
||||||
|
this.atlasTextures[id] = texture;
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isGlyphAvailable(char chr) {
|
public boolean isGlyphAvailable(char chr) {
|
||||||
if (this.font == null) { return false; }
|
if (this.font == null) { return false; }
|
||||||
return (getAtlas(chr).glyphData[chr % ATLAS_SIZE] != null);
|
return getAtlas(chr).has(chr % ATLAS_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A random glyph of the same width, for {@code §k}. Kept inside the character's own
|
* A random glyph of the same width, for {@code §k}. Kept inside the character's own
|
||||||
* atlas: atlases are rasterized on first use, so reaching outside one would build a
|
* atlas: reaching outside one would measure and rasterize another atlas mid-frame.
|
||||||
* new atlas mid-frame. Slots the font has no glyph for are skipped.
|
* Slots the font has no glyph for are skipped.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public char getRandomReplacement(char chr) {
|
public char getRandomReplacement(char chr) {
|
||||||
@@ -256,49 +270,52 @@ public final class FontProviderCustom implements FontProvider {
|
|||||||
return chr;
|
return chr;
|
||||||
}
|
}
|
||||||
final FontAtlas atlas = getAtlas(chr);
|
final FontAtlas atlas = getAtlas(chr);
|
||||||
final GlyphData target = atlas.glyphData[chr % ATLAS_SIZE];
|
if (!atlas.has(chr % ATLAS_SIZE)) {
|
||||||
if (target == null) {
|
|
||||||
return chr;
|
return chr;
|
||||||
}
|
}
|
||||||
|
final float targetAdvance = atlas.metrics[(chr % ATLAS_SIZE) * FontAtlas.STRIDE + FontAtlas.X_ADVANCE];
|
||||||
final int atlasStart = (chr / ATLAS_SIZE) * ATLAS_SIZE;
|
final int atlasStart = (chr / ATLAS_SIZE) * ATLAS_SIZE;
|
||||||
for (int attempt = 0; attempt < RANDOM_GLYPH_TRIES; attempt++) {
|
for (int attempt = 0; attempt < RANDOM_GLYPH_TRIES; attempt++) {
|
||||||
final int slot = fontRandom.nextInt(ATLAS_SIZE);
|
final int slot = fontRandom.nextInt(ATLAS_SIZE);
|
||||||
final GlyphData candidate = atlas.glyphData[slot];
|
if (atlas.has(slot) && atlas.metrics[slot * FontAtlas.STRIDE + FontAtlas.X_ADVANCE] == targetAdvance) {
|
||||||
if (candidate != null && candidate.xAdvance == target.xAdvance) {
|
|
||||||
return (char) (atlasStart + slot);
|
return (char) (atlasStart + slot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return chr;
|
return chr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float metric(char chr, int field) {
|
||||||
|
return getAtlas(chr).metrics[(chr % ATLAS_SIZE) * FontAtlas.STRIDE + field];
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float getUStart(char chr) {
|
public float getUStart(char chr) {
|
||||||
return getAtlas(chr).glyphData[chr % ATLAS_SIZE].uStart;
|
return metric(chr, FontAtlas.U_START);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float getVStart(char chr) {
|
public float getVStart(char chr) {
|
||||||
return getAtlas(chr).glyphData[chr % ATLAS_SIZE].vStart;
|
return metric(chr, FontAtlas.V_START);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float getXAdvance(char chr) {
|
public float getXAdvance(char chr) {
|
||||||
return getAtlas(chr).glyphData[chr % ATLAS_SIZE].xAdvance * FontConfig.customFontScale;
|
return metric(chr, FontAtlas.X_ADVANCE) * FontConfig.customFontScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float getGlyphW(char chr) {
|
public float getGlyphW(char chr) {
|
||||||
return getAtlas(chr).glyphData[chr % ATLAS_SIZE].glyphW * FontConfig.customFontScale;
|
return metric(chr, FontAtlas.GLYPH_W) * FontConfig.customFontScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float getUSize(char chr) {
|
public float getUSize(char chr) {
|
||||||
return getAtlas(chr).glyphData[chr % ATLAS_SIZE].uSz;
|
return metric(chr, FontAtlas.U_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float getVSize(char chr) {
|
public float getVSize(char chr) {
|
||||||
return getAtlas(chr).glyphData[chr % ATLAS_SIZE].vSz;
|
return metric(chr, FontAtlas.V_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -308,7 +325,9 @@ public final class FontProviderCustom implements FontProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getTexture(char chr) {
|
public int getTexture(char chr) {
|
||||||
return getAtlas(chr).texture;
|
final int id = chr / ATLAS_SIZE;
|
||||||
|
final int texture = this.atlasTextures[id];
|
||||||
|
return texture != 0 ? texture : uploadAtlas(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package com.gtnewhorizons.angelica.client.font;
|
||||||
|
|
||||||
|
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||||
|
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
|
||||||
|
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||||
|
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
|
||||||
|
import com.gtnewhorizons.angelica.glsm.testutil.TestThreads;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.lwjgl.BufferUtils;
|
||||||
|
import org.lwjgl.opengl.GL11;
|
||||||
|
import org.lwjgl.opengl.GL12;
|
||||||
|
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.nio.IntBuffer;
|
||||||
|
import java.util.concurrent.CyclicBarrier;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
@GLCoreTest
|
||||||
|
class FontProviderCustomGLTest {
|
||||||
|
|
||||||
|
private static final int QUALITY = 30;
|
||||||
|
private static final int THREADS = 8;
|
||||||
|
private static final char SURROGATE = '\uD800';
|
||||||
|
|
||||||
|
private final Font font = new Font(Font.DIALOG, Font.PLAIN, QUALITY);
|
||||||
|
private FontProviderCustom provider;
|
||||||
|
private float scaleBefore;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
scaleBefore = FontConfig.customFontScale;
|
||||||
|
FontConfig.customFontScale = 1.0f;
|
||||||
|
provider = new FontProviderCustom(font);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
FontConfig.customFontScale = scaleBefore;
|
||||||
|
provider.setFont(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] textures() {
|
||||||
|
return Reflect.get(provider, "atlasTextures");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object atlas(char chr) {
|
||||||
|
return Reflect.<Object[]>get(provider, "fontAtlases")[chr / FontProviderCustom.ATLAS_SIZE];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void metricsFromThreadWithoutContext() throws InterruptedException {
|
||||||
|
TestThreads.run("font-no-context", () -> {
|
||||||
|
assertTrue(provider.isGlyphAvailable('A'), "glyph not reported available off-thread");
|
||||||
|
assertTrue(provider.getXAdvance('A') > 0.0f, "no advance measured off-thread");
|
||||||
|
});
|
||||||
|
assertArrayEquals(new int[FontProviderCustom.ATLAS_COUNT], textures(), "measuring created a texture");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getTextureWithoutContextThrows() throws InterruptedException {
|
||||||
|
assertTrue(provider.isGlyphAvailable('A'));
|
||||||
|
TestThreads.run("font-no-context", () -> assertThrows(IllegalStateException.class, () -> provider.getTexture('A')));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void textureCreatedOnceOnFirstDraw() {
|
||||||
|
assertTrue(provider.isGlyphAvailable('A'));
|
||||||
|
final int texture = provider.getTexture('A');
|
||||||
|
assertNotEquals(0, texture, "no texture created on first draw-path lookup");
|
||||||
|
assertTrue(GL11.glIsTexture(texture), "atlas id is not a texture");
|
||||||
|
assertEquals(texture, provider.getTexture('A'), "atlas texture recreated on second lookup");
|
||||||
|
assertEquals(texture, provider.getTexture('B'), "same block resolved to another texture");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void glyphInkLandsInsideMeasuredRects() {
|
||||||
|
assertTrue(provider.isGlyphAvailable('A'));
|
||||||
|
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, provider.getTexture('A'));
|
||||||
|
final int width = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, 0, GL11.GL_TEXTURE_WIDTH);
|
||||||
|
final int height = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, 0, GL11.GL_TEXTURE_HEIGHT);
|
||||||
|
final IntBuffer pixels = BufferUtils.createIntBuffer(width * height);
|
||||||
|
GLStateManager.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, pixels);
|
||||||
|
|
||||||
|
final int margin = QUALITY / 6;
|
||||||
|
final boolean[] covered = new boolean[width * height];
|
||||||
|
for (int i = 0; i < FontProviderCustom.ATLAS_SIZE; i++) {
|
||||||
|
final char chr = (char) i;
|
||||||
|
if (!provider.isGlyphAvailable(chr)) continue;
|
||||||
|
final int x0 = (int) Math.floor(provider.getUStart(chr) * width);
|
||||||
|
final int x1 = (int) Math.ceil((provider.getUStart(chr) + provider.getUSize(chr)) * width);
|
||||||
|
final int y0 = (int) Math.floor(provider.getVStart(chr) * height);
|
||||||
|
final int y1 = (int) Math.ceil((provider.getVStart(chr) + provider.getVSize(chr)) * height);
|
||||||
|
int ink = 0;
|
||||||
|
for (int y = Math.max(0, y0 - margin); y < Math.min(height, y1 + margin); y++) {
|
||||||
|
for (int x = Math.max(0, x0 - margin); x < Math.min(width, x1 + margin); x++) {
|
||||||
|
covered[y * width + x] = true;
|
||||||
|
if (x >= x0 && x < x1 && y >= y0 && y < y1 && (pixels.get(y * width + x) >>> 24) != 0) ink++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Character.isLetterOrDigit(chr)) assertTrue(ink > 0, "no ink inside the measured rect of '" + chr + "'");
|
||||||
|
}
|
||||||
|
int stray = 0;
|
||||||
|
for (int i = 0; i < covered.length; i++) {
|
||||||
|
if (!covered[i] && (pixels.get(i) >>> 24) != 0) stray++;
|
||||||
|
}
|
||||||
|
assertEquals(0, stray, "ink outside every measured glyph rect");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void setFontDropsAtlasesAndTextures() {
|
||||||
|
assertTrue(provider.isGlyphAvailable('A'));
|
||||||
|
final Object atlasBefore = atlas('A');
|
||||||
|
assertNotEquals(0, provider.getTexture('A'));
|
||||||
|
|
||||||
|
provider.setFont(font.deriveFont(20.0f));
|
||||||
|
assertArrayEquals(new int[FontProviderCustom.ATLAS_COUNT], textures(), "font change kept a texture");
|
||||||
|
assertNull(atlas('A'), "font change kept an atlas");
|
||||||
|
assertTrue(provider.isGlyphAvailable('A'));
|
||||||
|
assertNotSame(atlasBefore, atlas('A'), "atlas not rebuilt for the new font");
|
||||||
|
assertTrue(GL11.glIsTexture(provider.getTexture('A')), "no texture for the new font");
|
||||||
|
|
||||||
|
provider.setFont(null);
|
||||||
|
assertFalse(provider.isGlyphAvailable('A'), "glyph available with no font set");
|
||||||
|
assertArrayEquals(new int[FontProviderCustom.ATLAS_COUNT], textures(), "clearing the font kept a texture");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void concurrentFirstTouchBuildsOneAtlas() throws Exception {
|
||||||
|
final CyclicBarrier start = new CyclicBarrier(THREADS);
|
||||||
|
final Object[] seen = new Object[THREADS];
|
||||||
|
final TestThreads.Worker[] workers = new TestThreads.Worker[THREADS];
|
||||||
|
for (int i = 0; i < THREADS; i++) {
|
||||||
|
final int slot = i;
|
||||||
|
workers[i] = TestThreads.start("font-metrics-race-" + i, () -> {
|
||||||
|
start.await();
|
||||||
|
assertTrue(provider.isGlyphAvailable('A'));
|
||||||
|
seen[slot] = atlas('A');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (TestThreads.Worker worker : workers) worker.join();
|
||||||
|
assertNotNull(seen[0], "atlas was not published");
|
||||||
|
for (int i = 1; i < THREADS; i++) assertSame(seen[0], seen[i], "threads observed different atlases");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void availabilityMatchesFont() {
|
||||||
|
for (int i = 0; i < FontProviderCustom.ATLAS_SIZE; i++) {
|
||||||
|
assertEquals(font.canDisplay((char) i), provider.isGlyphAvailable((char) i), "availability differs from the font for slot " + i);
|
||||||
|
assertEquals(font.canDisplay((char) (SURROGATE + i)), provider.isGlyphAvailable((char) (SURROGATE + i)), "availability differs from the font for surrogate slot " + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void randomReplacementKeepsWidth() {
|
||||||
|
final float advance = provider.getXAdvance('A');
|
||||||
|
for (int i = 0; i < 200; i++) {
|
||||||
|
final char replacement = provider.getRandomReplacement('A');
|
||||||
|
assertTrue(replacement < FontProviderCustom.ATLAS_SIZE, "replacement left the atlas");
|
||||||
|
assertTrue(provider.isGlyphAvailable(replacement), "replacement has no glyph");
|
||||||
|
assertEquals(advance, provider.getXAdvance(replacement), 0.0f, "replacement has another width");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user