mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-15 01:34:48 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bbb0537ee | ||
|
|
a019b7eed8 | ||
|
|
1772e9eae2 | ||
|
|
17a2ed7f37 | ||
|
|
34ed1d101b | ||
|
|
7cc9cea337 | ||
|
|
6c5e146f96 | ||
|
|
ea50e0a0b6 |
No files matched your search
@@ -1,14 +1,14 @@
|
||||
# Description of your *PR* and other info
|
||||
<!-- Please include a summary of the change -->
|
||||
## Summary
|
||||
<!-- Briefly describe what this PR changes and why. -->
|
||||
<!-- Any details that you think are important to review this PR? -->
|
||||
<!-- Are there any Issues / PRs related to this one? -->
|
||||
|
||||
<!-- Add a screenshot or a video demonstration for new features -->
|
||||
|
||||
<details open><summary>Template</summary> <!-- OPEN is intentional -->
|
||||
|
||||
### Before submitting a *pull request* make sure you have:
|
||||
- [ ] Read the [AI Contribution Policy](https://github.com/GTNewHorizons/Angelica/blob/master/AI_POLICY.md)
|
||||
- [ ] At least [skimmed](https://github.com/GTNewHorizons/Angelica/search?q=is%3Apr&type=Issues) the tracker for similar pull requests
|
||||
|
||||
</details>
|
||||
## Checklist
|
||||
- [ ] I have tested this PR in DevEnv
|
||||
- [ ] I have tested this PR in Fullpack
|
||||
- [ ] This PR is in compliance with the [GTNH AI Policy](https://github.com/GTNewHorizons/GTNH-Dev-Doc/blob/master/AI_POLICY.md)
|
||||
- [ ] I have at least [skimmed](https://github.com/GTNewHorizons/Angelica/search?q=is%3Apr&type=Issues) the tracker for similar pull requests
|
||||
- [ ] This PR requires another PR in order to merge
|
||||
@@ -175,8 +175,14 @@ val glsmRedirectAgent: Configuration by configurations.creating {
|
||||
isCanBeResolved = true
|
||||
}
|
||||
|
||||
val glsmMutableStaticsAgent: Configuration by configurations.creating {
|
||||
isCanBeConsumed = false
|
||||
isCanBeResolved = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
glsmRedirectAgent(project(":glsm", "redirectAgent"))
|
||||
glsmMutableStaticsAgent(project(":glsm", "mutableStaticsAgent"))
|
||||
archScanAsm(libs.asm.tree.test)
|
||||
archScanAsm(libs.asm.commons.test)
|
||||
}
|
||||
@@ -189,6 +195,7 @@ tasks.test {
|
||||
useJUnitPlatform { excludeTags = setOf("gl-core") }
|
||||
configureAngelicaJava8()
|
||||
classpath = archScanAsm + classpath
|
||||
javaAgent(glsmMutableStaticsAgent)
|
||||
jvmArgs("-Dceleritas.lwjglService=org.taumc.celeritas.lwjgl.HeadlessTestLWJGLService")
|
||||
dependsOn(":glsm:classes", ":lwjgl3-backend:classes", ":sdl-gpu:classes")
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ DependencyHandlerScope.of(dependencies).apply {
|
||||
runtimeOnlyNonPublishable("com.github.GTNewHorizons:BetterLoadingScreen:1.7.9-GTNH:dev")
|
||||
runtimeOnlyNonPublishable("com.github.GTNewHorizons:BetterQuesting:3.8.72-GTNH:dev")
|
||||
runtimeOnlyNonPublishable("com.github.GTNewHorizons:BuildCraft:7.1.63:dev")
|
||||
compileOnly("com.github.GTNewHorizons:Botania:1.13.32-GTNH:dev") { isTransitive = false }
|
||||
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Botania:1.13.32-GTNH:dev")
|
||||
runtimeOnlyNonPublishable("com.github.GTNewHorizons:LogisticsPipes:1.5.32-GTNH:dev")
|
||||
runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenComputers:1.12.64-GTNH:dev")
|
||||
|
||||
@@ -158,6 +158,7 @@ dependencies {
|
||||
testFixturesApi(platform(libs.junit.bom))
|
||||
testFixturesApi(libs.junit.jupiter.api)
|
||||
testFixturesApi(libs.lwjgl2)
|
||||
testFixturesCompileOnly(libs.asm.tree.test)
|
||||
|
||||
// Test
|
||||
testImplementation(sourceSets["stubs"].output)
|
||||
@@ -257,6 +258,13 @@ fun Test.configureGlsmJava8() {
|
||||
}
|
||||
}
|
||||
|
||||
val mutableStaticsAgentJar by tasks.registering(Jar::class) {
|
||||
archiveClassifier = "mutable-statics-agent"
|
||||
manifest {
|
||||
attributes("Premain-Class" to "com.gtnewhorizons.angelica.glsm.testutil.MutableStatics")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform {
|
||||
excludeTags = GL_TASK_TAGS.values.toSet() + "lwjgl3"
|
||||
@@ -265,6 +273,7 @@ tasks.test {
|
||||
excludeTestsMatching("com.gtnewhorizons.angelica.glsm.shader.SpirvShaderTranslator*Test")
|
||||
}
|
||||
configureGlsmJava8()
|
||||
javaAgent(files(mutableStaticsAgentJar))
|
||||
}
|
||||
|
||||
val redirectAgentJar by tasks.registering(Jar::class) {
|
||||
@@ -279,8 +288,14 @@ val redirectAgent by configurations.creating {
|
||||
isCanBeResolved = false
|
||||
}
|
||||
|
||||
val mutableStaticsAgent by configurations.creating {
|
||||
isCanBeConsumed = true
|
||||
isCanBeResolved = false
|
||||
}
|
||||
|
||||
artifacts {
|
||||
add(redirectAgent.name, redirectAgentJar)
|
||||
add(mutableStaticsAgent.name, mutableStaticsAgentJar)
|
||||
}
|
||||
|
||||
val glTestTasks = GL_TASK_TAGS.map { (taskName, tag) ->
|
||||
|
||||
@@ -69,7 +69,13 @@ public final class SystemProperties {
|
||||
public static final boolean FLYBY_JFR = Boolean.getBoolean("angelica.flyby.jfr");
|
||||
public static final float FLYBY_PITCH = parseFloat("angelica.flyby.pitch");
|
||||
public static final int FLYBY_SCREENSHOTS = Integer.getInteger("angelica.flyby.screenshots", 0);
|
||||
public static final boolean FLYBY_DEBUG_HUD = Boolean.getBoolean("angelica.flyby.debugHud");
|
||||
public static final Boolean FLYBY_DEBUG_HUD = parseBooleanOverride("angelica.flyby.debugHud");
|
||||
public static final String FLYBY_DEBUG_PROFILE = System.getProperty("angelica.flyby.debugProfile", "");
|
||||
public static final String FLYBY_DEBUG_ENTRIES = System.getProperty("angelica.flyby.debugEntries", "");
|
||||
public static final Boolean FLYBY_DEBUG_FLAT = parseBooleanOverride("angelica.flyby.debugFlat");
|
||||
public static final Boolean FLYBY_HUD_CACHING = parseBooleanOverride("angelica.flyby.hudCaching");
|
||||
public static final Boolean FLYBY_FPS_GRAPH = parseBooleanOverride("angelica.flyby.fpsGraph");
|
||||
public static final boolean FLYBY_DEBUG_DUMP = Boolean.getBoolean("angelica.flyby.debugDump");
|
||||
public static final String FLYBY_CRASH_TEST = System.getProperty("angelica.flyby.crashTest", "");
|
||||
|
||||
// Debug
|
||||
|
||||
@@ -275,6 +275,7 @@ public class GLContextState {
|
||||
public float savedFilterAniso = -1.0f;
|
||||
public boolean lineQuadsActive = false;
|
||||
public boolean lineStippleActive = false;
|
||||
public int textureSupersamplingSamples = 1;
|
||||
public Instancing ffpInstancing = Instancing.NONE;
|
||||
public float lastBrightnessX;
|
||||
public float lastBrightnessY;
|
||||
|
||||
@@ -653,6 +653,28 @@ public class GLStateManager {
|
||||
ctx().ffpInstancing = mode;
|
||||
}
|
||||
|
||||
public static int setTextureSupersamplingSamples(int samples) {
|
||||
if (samples < 1 || samples > 16 || (samples & (samples - 1)) != 0) {
|
||||
throw new IllegalArgumentException("Texture supersampling requires 1, 2, 4, 8, or 16 samples");
|
||||
}
|
||||
final GLContextState glCtx = ctx();
|
||||
final int previous = glCtx.textureSupersamplingSamples;
|
||||
if (previous != samples) {
|
||||
beforeUncapturedStateChange();
|
||||
glCtx.textureSupersamplingSamples = samples;
|
||||
}
|
||||
return previous;
|
||||
}
|
||||
|
||||
public static void markTextureAsAtlas(int texture, int gutter) {
|
||||
if (texture <= 0) return;
|
||||
final TextureInfo info = TextureInfoCache.INSTANCE.getInfo(texture);
|
||||
if (info.getAtlasGutter() != gutter) {
|
||||
beforeUncapturedStateChange();
|
||||
info.setAtlasGutter(gutter);
|
||||
}
|
||||
}
|
||||
|
||||
private static final MethodHandle MAT4_STACK_CURR_DEPTH;
|
||||
|
||||
static {
|
||||
@@ -3708,6 +3730,7 @@ public class GLStateManager {
|
||||
if (recordMode == RecordMode.COMPILE) {
|
||||
return;
|
||||
}
|
||||
DisplayListManager.flushAll();
|
||||
savedRecorder = DisplayListManager.pauseRecording();
|
||||
} else if (FeedbackManager.isFeedbackMode()) {
|
||||
FeedbackManager.processDrawArrays(mode, first, count);
|
||||
@@ -5310,7 +5333,7 @@ public class GLStateManager {
|
||||
RENDER_BACKEND.frontFace(glCtx.polygonState.getFrontFace());
|
||||
|
||||
final SamplerUnitArray samplerUnits = glCtx.samplerUnits;
|
||||
for (int i = 0; i < samplerUnits.size(); i++) {
|
||||
for (int i = 0; i <= samplerUnits.getHighestBoundUnit(); i++) {
|
||||
final int sampler = samplerUnits.get(i);
|
||||
if (sampler != 0) RENDER_BACKEND.bindSampler(i, sampler);
|
||||
}
|
||||
@@ -6437,8 +6460,11 @@ public class GLStateManager {
|
||||
}
|
||||
|
||||
public static void glBindSampler(int unit, int sampler) {
|
||||
bindSampler(ctx().samplerUnits, unit, sampler);
|
||||
}
|
||||
|
||||
private static void bindSampler(SamplerUnitArray units, int unit, int sampler) {
|
||||
rejectExitCapture();
|
||||
final SamplerUnitArray units = ctx().samplerUnits;
|
||||
if (isCachingEnabled() && unit >= 0 && unit < units.size() && units.get(unit) == sampler) return;
|
||||
beforeUncapturedStateChange();
|
||||
if (!units.set(unit, sampler)) {
|
||||
@@ -6447,6 +6473,16 @@ public class GLStateManager {
|
||||
RENDER_BACKEND.bindSampler(unit, sampler);
|
||||
}
|
||||
|
||||
public static void unbindAllSamplers() {
|
||||
final SamplerUnitArray units = ctx().samplerUnits;
|
||||
if (units.getHighestBoundUnit() < 0) return;
|
||||
beforeUncapturedStateChange();
|
||||
for (int unit = units.getHighestBoundUnit(); unit >= 0; unit = units.getHighestBoundUnit()) {
|
||||
units.set(unit, 0);
|
||||
RENDER_BACKEND.bindSampler(unit, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static void glBindSamplers(int first, int count, IntBuffer samplers) {
|
||||
if (samplers == null) {
|
||||
for (int i = 0; i < count; i++) glBindSampler(first + i, 0);
|
||||
|
||||
@@ -158,8 +158,8 @@ public class RenderSystem {
|
||||
maxSSBOBindings = 0;
|
||||
}
|
||||
|
||||
// Check for sampler objects support (GL 3.3+ or ARB extension)
|
||||
supportsSamplerObjects = GLStateManager.capabilities.OpenGL33 || GLStateManager.capabilities.GL_ARB_sampler_objects;
|
||||
supportsSamplerObjects = isGLES ? glesVersion >= 300
|
||||
: GLStateManager.capabilities.OpenGL33 || GLStateManager.capabilities.GL_ARB_sampler_objects;
|
||||
if (supportsSamplerObjects) {
|
||||
maxCombinedTextureImageUnits = RENDER_BACKEND.getInteger(GL20.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS);
|
||||
}
|
||||
@@ -713,12 +713,7 @@ public class RenderSystem {
|
||||
|
||||
public static void unbindAllSamplers() {
|
||||
if (!supportsSamplerObjects) return;
|
||||
final int count = GLStateManager.getSamplerUnitCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (GLStateManager.getSamplerBinding(i) != 0) {
|
||||
GLStateManager.glBindSampler(i, 0);
|
||||
}
|
||||
}
|
||||
GLStateManager.unbindAllSamplers();
|
||||
}
|
||||
|
||||
/** Parse a GL version string (e.g. "4.6.0 NVIDIA ...") into concatenated digits "460". Strips
|
||||
|
||||
+6
-1
@@ -42,6 +42,9 @@ import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetAddress;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.Pointer.POINTER_SIZE;
|
||||
import static org.taumc.celeritas.lwjgl.LWJGLServiceProvider.LWJGL;
|
||||
|
||||
/**
|
||||
@@ -380,7 +383,9 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
|
||||
|
||||
@Override
|
||||
public void multiDrawElementsBaseVertex(int mode, long pCount, int type, long pIndices, int drawcount, long pBaseVertex) {
|
||||
throw new UnsupportedOperationException("GLRenderBackend.multiDrawElementsBaseVertex should not be called — LWJGL service handles this");
|
||||
for (int i = 0; i < drawcount; i++) {
|
||||
GL32.glDrawElementsBaseVertex(mode, memGetInt(pCount + 4L * i), type, memGetAddress(pIndices + (long) i * POINTER_SIZE), memGetInt(pBaseVertex + 4L * i));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,6 +26,8 @@ import java.util.function.Predicate;
|
||||
*/
|
||||
public abstract class RenderBackend {
|
||||
|
||||
public static final int MAX_FRAMES_IN_FLIGHT = 3;
|
||||
|
||||
public void onPostWindowCreate(long window) {}
|
||||
|
||||
public abstract void init();
|
||||
|
||||
+3
-2
@@ -11,13 +11,14 @@ import java.util.function.IntSupplier;
|
||||
* @param samplerNames samplers the graphics program may not have, because only the stripped writes read them
|
||||
* @param samplerUnits per sampler name, the texture unit it reads at each draw, or -1 to use samplerTextures
|
||||
* @param samplerTextures per sampler name, its texture when samplerUnits is -1
|
||||
* @param samplerObjects per sampler name, the GL sampler object to sample it with when samplerUnits is -1, or 0
|
||||
* @param samplerObjects per sampler name, supplies its current GL sampler object when samplerUnits is -1;
|
||||
* null for unassigned or external bindings, and returns 0 to use the texture's own filtering
|
||||
* @param vertexBufferBinding storage buffer binding of the first vertex buffer the replay program declares
|
||||
* @param vertexBufferCount how many vertex buffers it declares, at consecutive bindings
|
||||
* @param indexBufferBinding storage buffer binding of its index buffer
|
||||
*/
|
||||
public record VertexWriteReplaySetup(int replayProgram, String[] inputNames, int[] inputLocations,
|
||||
IntSupplier[] images, int writtenImageCount,
|
||||
String[] samplerNames, int[] samplerUnits, IntSupplier[] samplerTextures, int[] samplerObjects,
|
||||
String[] samplerNames, int[] samplerUnits, IntSupplier[] samplerTextures, IntSupplier[] samplerObjects,
|
||||
int vertexBufferBinding, int vertexBufferCount, int indexBufferBinding) {
|
||||
}
|
||||
@@ -104,6 +104,7 @@ public final class FFPUniformBlock {
|
||||
public static final int TEX_ENV_COLOR_3 = vec4("u_TexEnvColor3");
|
||||
public static final int OVERLAY_COLOR = vec4("u_OverlayColor");
|
||||
public static final int SECONDARY_COLOR = vec3("u_SecondaryColor");
|
||||
public static final int ATLAS_GUTTER = flt("u_AtlasGutter");
|
||||
public static final int FOG_PARAMS = vec4("u_FogParams");
|
||||
public static final int FOG_COLOR = vec4("u_FogColor");
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.gtnewhorizons.angelica.glsm.GLContextState;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.states.TexEnvState;
|
||||
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
|
||||
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL12;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
@@ -82,7 +83,7 @@ public final class FragmentKey {
|
||||
private static final int BIT_ALPHA_TEST = 2; // 1 bit
|
||||
private static final int BIT_ALPHA_FUNC = 3; // 3 bits
|
||||
private static final int BIT_SEPARATE_SPECULAR = 6; // 1 bit
|
||||
private static final int BIT_NR_ENABLED_UNITS = 7; // 3 bits
|
||||
private static final int BIT_TEXTURE_SUPERSAMPLING = 7; // 3 bits, log2(samples)
|
||||
private static final int BIT_OVERLAY_ENABLED = 10; // 1 bit
|
||||
private static final int BIT_LINE_STIPPLE = 11; // 1 bit
|
||||
private static final int BIT_COLOR_SUM = 12; // 1 bit
|
||||
@@ -172,7 +173,10 @@ public final class FragmentKey {
|
||||
}
|
||||
|
||||
final int nrEnabled = highestEnabled + 1;
|
||||
global |= ((long) (nrEnabled & 0x7)) << BIT_NR_ENABLED_UNITS;
|
||||
if (glCtx.textureSupersamplingSamples > 1 && (unit0Bits & 1L) != 0
|
||||
&& TextureInfoCache.INSTANCE.getInfo(textures.getTextureUnitBindings(0).getBinding()).getAtlasGutter() > 0) {
|
||||
global |= (long) Integer.numberOfTrailingZeros(glCtx.textureSupersamplingSamples) << BIT_TEXTURE_SUPERSAMPLING;
|
||||
}
|
||||
global |= (unit0Bits << GLOBAL_BITS);
|
||||
out[0] = global;
|
||||
|
||||
@@ -256,12 +260,13 @@ public final class FragmentKey {
|
||||
public int fogMode() { return (int) (packed[0] & 0x3); }
|
||||
public boolean combinedGlint() { return (packed[0] & (1L << BIT_COMBINED_GLINT)) != 0; }
|
||||
public boolean glintReplaceAlpha() { return (packed[0] & (1L << BIT_GLINT_REPLACE_ALPHA)) != 0; }
|
||||
public int textureSupersamplingSamples() { return 1 << (int) ((packed[0] >> BIT_TEXTURE_SUPERSAMPLING) & 0x7); }
|
||||
public boolean alphaTestEnabled() { return ((packed[0] >> BIT_ALPHA_TEST) & 1) != 0; }
|
||||
public int alphaTestFunc() { return (int) ((packed[0] >> BIT_ALPHA_FUNC) & 0x7); }
|
||||
public boolean separateSpecular() { return ((packed[0] >> BIT_SEPARATE_SPECULAR) & 1) != 0; }
|
||||
public boolean colorSum() { return ((packed[0] >> BIT_COLOR_SUM) & 1) != 0; }
|
||||
public boolean lineStipple() { return ((packed[0] >> BIT_LINE_STIPPLE) & 1) != 0; }
|
||||
public int nrEnabledUnits() { return (int) ((packed[0] >> BIT_NR_ENABLED_UNITS) & 0x7); }
|
||||
public int nrEnabledUnits() { return packed.length == 1 && !unitEnabled(0) ? 0 : packed.length; }
|
||||
public boolean overlayEnabled() { return ((packed[0] >> BIT_OVERLAY_ENABLED) & 1) != 0; }
|
||||
public boolean overlayInstanced() { return ((packed[0] >> BIT_OVERLAY_INSTANCED) & 1) != 0; }
|
||||
|
||||
@@ -400,6 +405,7 @@ public final class FragmentKey {
|
||||
sb.append(String.format("FFPFragmentKey[fog=%s alpha=%b(%s) specSep=%b colorSum=%b overlay=%b units=%d", fogName, alphaTestEnabled(),
|
||||
alphaTestEnabled() ? String.format("0x%04X", decodeAlphaFunc(alphaTestFunc())) : "-", separateSpecular(), colorSum(),
|
||||
overlayEnabled() || overlayInstanced(), nrEnabledUnits()));
|
||||
if (textureSupersamplingSamples() > 1) sb.append(" samples=").append(textureSupersamplingSamples());
|
||||
for (int i = 0; i < nrEnabledUnits(); i++) {
|
||||
if (!unitEnabled(i)) {
|
||||
sb.append(String.format(" u%d=OFF", i));
|
||||
|
||||
@@ -28,6 +28,10 @@ public final class FragmentShaderGenerator {
|
||||
return sb.toString();
|
||||
}
|
||||
sb.append("out vec4 fragColor;\n\n");
|
||||
if (key.textureSupersamplingSamples() > 1) {
|
||||
sb.append("#define TEXTURE_SUPERSAMPLING_SAMPLES ").append(key.textureSupersamplingSamples()).append('\n');
|
||||
sb.append(ShaderLoader.getShaderSource("angelica:texture_supersampling.glsl"));
|
||||
}
|
||||
|
||||
sb.append("void main() {\n");
|
||||
emitLineStipple(sb, key);
|
||||
@@ -86,7 +90,8 @@ public final class FragmentShaderGenerator {
|
||||
private static void emitTextureSampling(StringBuilder sb, FragmentKey key) {
|
||||
for (int i = 0; i < key.nrEnabledUnits(); i++) {
|
||||
if (!key.unitEnabled(i)) continue;
|
||||
sb.append(" vec4 tex").append(i).append("Color = textureProj(u_Sampler").append(i).append(", v_TexCoord").append(i).append(");\n");
|
||||
final String sample = i == 0 && key.textureSupersamplingSamples() > 1 ? "sampleSupersampledTexture" : "textureProj";
|
||||
sb.append(" vec4 tex").append(i).append("Color = ").append(sample).append("(u_Sampler").append(i).append(", v_TexCoord").append(i).append(");\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -190,6 +190,10 @@ public final class ShaderManager {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isTextureSupersampling() {
|
||||
return currentProgram != null && currentProgram.getFragmentKey().textureSupersamplingSamples() > 1;
|
||||
}
|
||||
|
||||
public static void endFrame() {
|
||||
final ShaderManager sm = GLStateManager.ctx().ffp;
|
||||
sm.lastFramePreDrawCalls = sm.preDrawCalls;
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.gtnewhorizons.angelica.glsm.states.TexGenState;
|
||||
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
|
||||
import com.gtnewhorizons.angelica.glsm.states.ViewportState;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.UniformRingBuffer;
|
||||
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
|
||||
import org.joml.Math;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Matrix4f;
|
||||
@@ -49,6 +50,7 @@ public class Uniforms {
|
||||
private int viewportX = Integer.MIN_VALUE, viewportY = Integer.MIN_VALUE, viewportWidth = -1, viewportHeight = -1;
|
||||
private int lineStipple = -1;
|
||||
private int weatherGen = -1;
|
||||
private int atlasGutter = -1;
|
||||
|
||||
int blockWrites;
|
||||
int blockSkips;
|
||||
@@ -170,6 +172,15 @@ public class Uniforms {
|
||||
stagedFragment++;
|
||||
dirty = true;
|
||||
}
|
||||
if (glCtx.textureSupersamplingSamples > 1 && ffp.isTextureSupersampling()) {
|
||||
final int gutter = TextureInfoCache.INSTANCE.getInfo(glCtx.textures.getTextureUnitBindings(0).getBinding()).getAtlasGutter();
|
||||
if (gutter != atlasGutter) {
|
||||
putFloat(FFPUniformBlock.ATLAS_GUTTER, gutter);
|
||||
atlasGutter = gutter;
|
||||
stagedMisc++;
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
final LineState line = glCtx.lineState;
|
||||
final float lw = line.getWidth();
|
||||
if (lw != lineWidth) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
|
||||
|
||||
/**
|
||||
* Generates GLSL 330 core vertex shaders for FFP emulation.
|
||||
@@ -19,6 +20,9 @@ public final class VertexShaderGenerator {
|
||||
emitAttributes(sb, key);
|
||||
emitUniforms(sb, key);
|
||||
emitOutputs(sb, key);
|
||||
if (key.lineQuads()) {
|
||||
sb.append(ShaderLoader.getShaderSource("angelica:include/line_quads.glsl")).append('\n');
|
||||
}
|
||||
|
||||
sb.append("void main() {\n");
|
||||
if (key.instancing() == Instancing.WEATHER) {
|
||||
@@ -142,7 +146,7 @@ public final class VertexShaderGenerator {
|
||||
} else {
|
||||
sb.append(" gl_Position = u_MVPMatrix * pos4;\n");
|
||||
if (key.lineQuads()) {
|
||||
emitLineQuadWidening(sb);
|
||||
emitLineQuadWidening(sb, key);
|
||||
}
|
||||
|
||||
// Eye position needed for lighting, fog, EYE_LINEAR texgen, and clip planes
|
||||
@@ -156,42 +160,15 @@ public final class VertexShaderGenerator {
|
||||
sb.append('\n');
|
||||
}
|
||||
|
||||
private static void emitLineQuadWidening(StringBuilder sb) {
|
||||
private static void emitLineQuadWidening(StringBuilder sb, VertexKey key) {
|
||||
sb.append(" int lineCorner = gl_VertexID % 6;\n");
|
||||
sb.append(" bool lineStart = lineCorner == 0 || lineCorner == 1 || lineCorner == 3;\n");
|
||||
sb.append(" bool lineStart = angelica_lineStart(lineCorner);\n");
|
||||
sb.append(" vec4 lineOther = u_MVPMatrix * vec4(a_LineOtherEnd, 1.0);\n");
|
||||
sb.append(" vec4 lineC0 = lineStart ? gl_Position : lineOther;\n");
|
||||
sb.append(" vec4 lineC1 = lineStart ? lineOther : gl_Position;\n");
|
||||
sb.append(" vec4 lineCenter = lineC0;\n");
|
||||
sb.append(" float lineT = 0.0;\n");
|
||||
sb.append(" float lineD0 = lineC0.z + lineC0.w;\n");
|
||||
sb.append(" float lineD1 = lineC1.z + lineC1.w;\n");
|
||||
sb.append(" if (lineD0 < 0.0 && lineD1 < 0.0) {\n");
|
||||
sb.append(" gl_Position = vec4(0.0, 0.0, 2.0, 1.0);\n");
|
||||
sb.append(" } else {\n");
|
||||
sb.append(" float lineT0 = lineD0 < 0.0 ? lineD0 / (lineD0 - lineD1) : 0.0;\n");
|
||||
sb.append(" float lineT1 = lineD1 < 0.0 ? lineD0 / (lineD0 - lineD1) : 1.0;\n");
|
||||
sb.append(" lineT = lineStart ? lineT0 : lineT1;\n");
|
||||
sb.append(" vec4 lineP0 = mix(lineC0, lineC1, lineT0);\n");
|
||||
sb.append(" vec4 lineP1 = mix(lineC0, lineC1, lineT1);\n");
|
||||
sb.append(" vec2 lineN0 = lineP0.xy / lineP0.w;\n");
|
||||
sb.append(" vec2 lineN1 = lineP1.xy / lineP1.w;\n");
|
||||
sb.append(" vec2 linePixel = 2.0 / u_ViewportSize;\n");
|
||||
sb.append(" float lineHalfWidth = 0.5 * u_LineWidth;\n");
|
||||
sb.append(" vec2 lineOffset;\n");
|
||||
sb.append(" vec2 lineShift;\n");
|
||||
sb.append(" vec2 lineSpan = abs(lineN1 - lineN0) * u_ViewportSize;\n");
|
||||
sb.append(" if (lineSpan.x > lineSpan.y) {\n");
|
||||
sb.append(" lineOffset = vec2(0.0, lineHalfWidth * linePixel.y);\n");
|
||||
sb.append(" lineShift = vec2(lineN0.x < lineN1.x ? -0.5 : 0.5, -0.125) * linePixel;\n");
|
||||
sb.append(" } else {\n");
|
||||
sb.append(" lineOffset = vec2(lineHalfWidth * linePixel.x, 0.0);\n");
|
||||
sb.append(" lineShift = vec2(0.125, lineN0.y < lineN1.y ? -0.5 : 0.5) * linePixel;\n");
|
||||
sb.append(" }\n");
|
||||
sb.append(" vec4 lineP = lineStart ? lineP0 : lineP1;\n");
|
||||
sb.append(" vec2 lineN = lineStart ? lineN0 : lineN1;\n");
|
||||
sb.append(" gl_Position = vec4((lineN + (lineCorner % 2 == 0 ? lineOffset : -lineOffset) + lineShift) * lineP.w, lineP.z, lineP.w);\n");
|
||||
sb.append(" }\n");
|
||||
if (key.lineStipple()) {
|
||||
sb.append(" vec4 lineCenter = lineStart ? gl_Position : lineOther;\n");
|
||||
}
|
||||
sb.append(" float lineT;\n");
|
||||
sb.append(" gl_Position = angelica_lineQuad(gl_Position, lineOther, lineCorner, u_ViewportSize, u_LineWidth, lineT);\n");
|
||||
}
|
||||
|
||||
private static void emitNormalTransform(StringBuilder sb, VertexKey key) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IVertexArrayObject;
|
||||
import com.gtnewhorizons.angelica.glsm.GLContextState;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.LineQuads;
|
||||
@@ -122,18 +121,14 @@ public final class DisplayListVBO {
|
||||
}
|
||||
|
||||
private void renderLineQuads() {
|
||||
final GLContextState glCtx = GLStateManager.ctx();
|
||||
lineQuads.bind();
|
||||
VAOManager.setCurrentVertexFlags(lineQuadFlags);
|
||||
final boolean culled = LineQuads.disableCulling();
|
||||
glCtx.lineQuadsActive = true;
|
||||
final boolean culled = LineQuads.beginDraw(lineQuadFlags);
|
||||
FfpExtendedAttribs.beginInternalDraw();
|
||||
try {
|
||||
lineQuads.draw(GL11.GL_TRIANGLES, lineQuadStart, lineQuadCount);
|
||||
} finally {
|
||||
FfpExtendedAttribs.endInternalDraw();
|
||||
glCtx.lineQuadsActive = false;
|
||||
LineQuads.restoreCulling(culled);
|
||||
LineQuads.endDraw(culled);
|
||||
}
|
||||
lineQuads.unbind();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.gtnewhorizons.angelica.glsm.states;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.RenderSystem;
|
||||
import lombok.Getter;
|
||||
|
||||
public class SamplerUnitArray {
|
||||
|
||||
private static final int MIN_UNITS = 16;
|
||||
|
||||
private int[] samplers;
|
||||
@Getter
|
||||
private int highestBoundUnit = -1;
|
||||
|
||||
public int get(int unit) {
|
||||
final int[] local = samplers;
|
||||
@@ -22,15 +25,27 @@ public class SamplerUnitArray {
|
||||
}
|
||||
if (unit >= local.length) return false;
|
||||
local[unit] = sampler;
|
||||
if (sampler != 0) {
|
||||
highestBoundUnit = Math.max(highestBoundUnit, unit);
|
||||
} else if (unit == highestBoundUnit) {
|
||||
trimUnboundUnits();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void unbindEverywhere(int sampler) {
|
||||
final int[] local = samplers;
|
||||
if (local == null) return;
|
||||
for (int i = 0; i < local.length; i++) {
|
||||
for (int i = 0; i <= highestBoundUnit; i++) {
|
||||
if (local[i] == sampler) local[i] = 0;
|
||||
}
|
||||
trimUnboundUnits();
|
||||
}
|
||||
|
||||
private void trimUnboundUnits() {
|
||||
while (highestBoundUnit >= 0 && samplers[highestBoundUnit] == 0) {
|
||||
highestBoundUnit--;
|
||||
}
|
||||
}
|
||||
|
||||
public int size() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memCopy;
|
||||
@@ -72,6 +73,18 @@ public final class LineQuads {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean beginDraw(int vertexFlags) {
|
||||
VAOManager.setCurrentVertexFlags(vertexFlags);
|
||||
final boolean culled = disableCulling();
|
||||
GLStateManager.ctx().lineQuadsActive = true;
|
||||
return culled;
|
||||
}
|
||||
|
||||
public static void endDraw(boolean culled) {
|
||||
GLStateManager.ctx().lineQuadsActive = false;
|
||||
restoreCulling(culled);
|
||||
}
|
||||
|
||||
public static VertexFormat quadFormat(VertexFormat lineFormat) {
|
||||
return QUAD_FORMATS[lineFormat.getVertexFlags() | VertexFlags.NORMAL_BIT];
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public class TextureInfo {
|
||||
protected int resolvedInternalFormat = -1;
|
||||
protected int width = -1;
|
||||
protected int height = -1;
|
||||
@Getter @Setter private int atlasGutter;
|
||||
|
||||
protected int minFilter = UNSET_INT;
|
||||
protected int magFilter = UNSET_INT;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
bool angelica_lineStart(int corner) {
|
||||
return corner == 0 || corner == 1 || corner == 3;
|
||||
}
|
||||
|
||||
vec4 angelica_lineQuad(vec4 clipThis, vec4 clipOther, int corner, vec2 viewport, float width, out float lineT) {
|
||||
bool lineStart = angelica_lineStart(corner);
|
||||
vec4 lineC0 = lineStart ? clipThis : clipOther;
|
||||
vec4 lineC1 = lineStart ? clipOther : clipThis;
|
||||
lineT = 0.0;
|
||||
float lineD0 = lineC0.z + lineC0.w;
|
||||
float lineD1 = lineC1.z + lineC1.w;
|
||||
if (lineD0 < 0.0 && lineD1 < 0.0) {
|
||||
return vec4(0.0, 0.0, 2.0, 1.0);
|
||||
} else {
|
||||
float lineT0 = lineD0 < 0.0 ? lineD0 / (lineD0 - lineD1) : 0.0;
|
||||
float lineT1 = lineD1 < 0.0 ? lineD0 / (lineD0 - lineD1) : 1.0;
|
||||
lineT = lineStart ? lineT0 : lineT1;
|
||||
vec4 lineP0 = mix(lineC0, lineC1, lineT0);
|
||||
vec4 lineP1 = mix(lineC0, lineC1, lineT1);
|
||||
vec2 lineN0 = lineP0.xy / lineP0.w;
|
||||
vec2 lineN1 = lineP1.xy / lineP1.w;
|
||||
vec2 linePixel = 2.0 / viewport;
|
||||
float lineHalfWidth = 0.5 * width;
|
||||
vec2 lineOffset;
|
||||
vec2 lineShift;
|
||||
vec2 lineSpan = abs(lineN1 - lineN0) * viewport;
|
||||
if (lineSpan.x > lineSpan.y) {
|
||||
lineOffset = vec2(0.0, lineHalfWidth * linePixel.y);
|
||||
lineShift = vec2(lineN0.x < lineN1.x ? -0.5 : 0.5, -0.125) * linePixel;
|
||||
} else {
|
||||
lineOffset = vec2(lineHalfWidth * linePixel.x, 0.0);
|
||||
lineShift = vec2(0.125, lineN0.y < lineN1.y ? -0.5 : 0.5) * linePixel;
|
||||
}
|
||||
vec4 lineP = lineStart ? lineP0 : lineP1;
|
||||
vec2 lineN = lineStart ? lineN0 : lineN1;
|
||||
return vec4((lineN + (corner % 2 == 0 ? lineOffset : -lineOffset) + lineShift) * lineP.w, lineP.z, lineP.w);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
const vec2 SAMPLE_OFFSETS[TEXTURE_SUPERSAMPLING_SAMPLES] = vec2[](
|
||||
#if TEXTURE_SUPERSAMPLING_SAMPLES == 2
|
||||
vec2(0.25, 0.25), vec2(-0.25, -0.25)
|
||||
#elif TEXTURE_SUPERSAMPLING_SAMPLES == 4
|
||||
vec2(0.125, 0.375), vec2(-0.125, -0.375),
|
||||
vec2(0.375, -0.125), vec2(-0.375, 0.125)
|
||||
#elif TEXTURE_SUPERSAMPLING_SAMPLES == 8
|
||||
vec2(-0.1875, -0.0625), vec2( 0.3125, 0.0625),
|
||||
vec2(-0.0625, 0.1875), vec2(-0.3125, -0.1875),
|
||||
vec2( 0.1875, -0.3125), vec2( 0.4375, -0.4375),
|
||||
vec2(-0.4375, 0.3125), vec2( 0.0625, 0.4375)
|
||||
#elif TEXTURE_SUPERSAMPLING_SAMPLES == 16
|
||||
vec2( 0.0625, 0.0625), vec2(-0.0625, -0.1875),
|
||||
vec2(-0.1875, 0.1250), vec2( 0.2500, -0.0625),
|
||||
vec2(-0.3125, -0.1250), vec2( 0.1250, 0.3125),
|
||||
vec2( 0.3125, 0.1875), vec2( 0.1875, -0.3125),
|
||||
vec2(-0.1250, 0.3750), vec2( 0.0000, -0.4375),
|
||||
vec2(-0.2500, -0.3750), vec2(-0.3750, 0.2500),
|
||||
vec2(-0.5000, 0.0000), vec2( 0.4375, -0.2500),
|
||||
vec2( 0.3750, 0.4375), vec2(-0.4375, -0.5000)
|
||||
#endif
|
||||
);
|
||||
|
||||
vec4 sampleSupersampledTexture(sampler2D tex, vec4 coord) {
|
||||
vec2 uv = coord.xy / coord.w;
|
||||
vec2 dx = dFdx(uv);
|
||||
vec2 dy = dFdy(uv);
|
||||
ivec2 size = textureSize(tex, 0);
|
||||
vec2 offsets[TEXTURE_SUPERSAMPLING_SAMPLES];
|
||||
vec2 extent = vec2(0.0);
|
||||
for (int i = 0; i < TEXTURE_SUPERSAMPLING_SAMPLES; i++) {
|
||||
offsets[i] = SAMPLE_OFFSETS[i].x * dx + SAMPLE_OFFSETS[i].y * dy;
|
||||
extent = max(extent, abs(offsets[i]));
|
||||
}
|
||||
extent *= vec2(size);
|
||||
float footprint = max(extent.x, extent.y);
|
||||
if (footprint >= u_AtlasGutter) return textureGrad(tex, uv, dx, dy);
|
||||
float supersampling = 1.0 - smoothstep(0.75 * u_AtlasGutter, u_AtlasGutter, footprint);
|
||||
|
||||
// The atlas gutter is 2^maxMipLevel.
|
||||
float texelsPerPixel = max(length(dx * vec2(size)), length(dy * vec2(size)));
|
||||
int lod = int(min(ceil(log2(max(texelsPerPixel / sqrt(float(TEXTURE_SUPERSAMPLING_SAMPLES)), 1.0))),
|
||||
log2(u_AtlasGutter)));
|
||||
|
||||
// Transparent texels must not tint the visible samples.
|
||||
vec4 color = vec4(0.0);
|
||||
for (int i = 0; i < TEXTURE_SUPERSAMPLING_SAMPLES; i++) {
|
||||
ivec2 texel = min(ivec2(fract(uv + offsets[i]) * vec2(size)), size - 1);
|
||||
vec4 sampleColor = texelFetch(tex, texel >> lod, lod);
|
||||
color += vec4(sampleColor.rgb * sampleColor.a, sampleColor.a);
|
||||
}
|
||||
color /= float(TEXTURE_SUPERSAMPLING_SAMPLES);
|
||||
if (supersampling < 1.0) {
|
||||
vec4 filtered = textureGrad(tex, uv, dx, dy);
|
||||
color = mix(vec4(filtered.rgb * filtered.a, filtered.a), color, supersampling);
|
||||
}
|
||||
return vec4(color.rgb / max(color.a, 0.00001), color.a);
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package com.gtnewhorizons.angelica.glsm;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.glsm.states.ViewportState;
|
||||
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.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/** The executed half of a GL_COMPILE_AND_EXECUTE glDrawArrays sees transforms issued since the last matrix barrier. */
|
||||
@GLCoreTest
|
||||
public class GLSM_DisplayList_CompileAndExecuteArrays_GLTest {
|
||||
|
||||
private static final int W = 32;
|
||||
|
||||
private static final int POSITION_LOC = VertexFormatElement.Usage.POSITION.getAttributeLocation();
|
||||
|
||||
private final ByteBuffer pixels = BufferUtils.createByteBuffer(W * 4);
|
||||
private int savedX, savedY, savedW, savedH;
|
||||
private int vao, vbo, list;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
final ViewportState vp = GLStateManager.getViewportState();
|
||||
savedX = vp.x;
|
||||
savedY = vp.y;
|
||||
savedW = vp.width;
|
||||
savedH = vp.height;
|
||||
|
||||
GLStateManager.glUseProgram(0);
|
||||
|
||||
vao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
vbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
final ByteBuffer verts = ByteBuffer.allocateDirect(3 * 12).order(ByteOrder.nativeOrder());
|
||||
verts.putFloat(0f).putFloat(0.5f).putFloat(0f).putFloat(W).putFloat(0.5f).putFloat(0f).flip();
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, verts, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glVertexAttribPointer(POSITION_LOC, 3, GL11.GL_FLOAT, false, 12, 0L);
|
||||
GLStateManager.glEnableVertexAttribArray(POSITION_LOC);
|
||||
|
||||
GLStateManager.glViewport(0, 0, W, 1);
|
||||
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
|
||||
GLStateManager.glPushMatrix();
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glOrtho(0, W, 0, 1, -1, 1);
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
GLStateManager.glPushMatrix();
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glDisable(GL11.GL_DEPTH_TEST);
|
||||
GLStateManager.glDisable(GL11.GL_BLEND);
|
||||
GLStateManager.glDisable(GL11.GL_TEXTURE_2D);
|
||||
GLStateManager.glColor4f(1f, 1f, 1f, 1f);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (list > 0) GLStateManager.glDeleteLists(list, 1);
|
||||
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
|
||||
GLStateManager.glPopMatrix();
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
GLStateManager.glPopMatrix();
|
||||
GLStateManager.glViewport(savedX, savedY, savedW, savedH);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
if (vbo != 0) GLStateManager.glDeleteBuffers(vbo);
|
||||
if (vao != 0) GLStateManager.glDeleteVertexArrays(vao);
|
||||
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
GLStateManager.glClearColor(0f, 0f, 0f, 1f);
|
||||
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
private boolean[] readCoverage() {
|
||||
pixels.clear();
|
||||
GLStateManager.glReadPixels(0, 0, W, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
|
||||
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "GL error during readback");
|
||||
|
||||
final boolean[] covered = new boolean[W];
|
||||
for (int i = 0; i < W; i++) {
|
||||
covered[i] = (pixels.get(i * 4) & 0xFF) > 127;
|
||||
}
|
||||
return covered;
|
||||
}
|
||||
|
||||
private static boolean[] leftHalf() {
|
||||
final boolean[] e = new boolean[W];
|
||||
for (int i = 0; i < W / 2; i++) e[i] = true;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** The scale is issued inside the list, after the push that flushed everything before it - AE2 monitor's shape. */
|
||||
private void compileScaledDraw(int mode) {
|
||||
list = GLStateManager.glGenLists(1);
|
||||
GLStateManager.glNewList(list, mode);
|
||||
GLStateManager.glPushMatrix();
|
||||
GLStateManager.glScalef(0.5f, 1f, 1f);
|
||||
GLStateManager.glDrawArrays(GL11.GL_LINES, 0, 2);
|
||||
GLStateManager.glPopMatrix();
|
||||
GLStateManager.glEndList();
|
||||
}
|
||||
|
||||
// Replay is not asserted here: a compiled list plays back through FFP client arrays, which this core context lacks,
|
||||
// and in the compat context this attribute setup draws nothing at all.
|
||||
@Test
|
||||
void compileAndExecuteDrawsWithPendingScale() {
|
||||
clear();
|
||||
compileScaledDraw(GL11.GL_COMPILE_AND_EXECUTE);
|
||||
assertArrayEquals(leftHalf(), readCoverage(), "executed half of GL_COMPILE_AND_EXECUTE");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.glsm;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.BatchStateGuard;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -10,6 +11,7 @@ import org.lwjgl.opengl.GL33;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
@@ -115,37 +117,88 @@ class GLSM_SamplerBinding_GLTest {
|
||||
@Test
|
||||
void unbindAllSamplersClearsUnitsBoundThroughGlsm() {
|
||||
final int sampler = RenderSystem.genSampler();
|
||||
final Runnable previousFlush = BatchStateGuard.flush;
|
||||
final AtomicInteger flushes = new AtomicInteger();
|
||||
try {
|
||||
GLStateManager.glBindSampler(1, sampler);
|
||||
GLStateManager.glBindSampler(3, sampler);
|
||||
BatchStateGuard.flush = () -> {
|
||||
assertEquals(sampler, driverBinding(1), "deferred draws must see the original bindings");
|
||||
assertEquals(sampler, driverBinding(3), "deferred draws must see the original bindings");
|
||||
flushes.incrementAndGet();
|
||||
};
|
||||
|
||||
RenderSystem.unbindAllSamplers();
|
||||
RenderSystem.unbindAllSamplers();
|
||||
|
||||
assertEquals(1, flushes.get(), "bulk unbind should drain deferred draws once");
|
||||
assertEquals(0, driverBinding(1), "unit 1 still bound on the driver");
|
||||
assertEquals(0, driverBinding(3), "unit 3 still bound on the driver");
|
||||
assertEquals(0, GLStateManager.getSamplerBinding(1));
|
||||
assertEquals(0, GLStateManager.getSamplerBinding(3));
|
||||
} finally {
|
||||
BatchStateGuard.flush = previousFlush;
|
||||
RenderSystem.destroySampler(sampler);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void replayForwardsCachedSamplerBindings() {
|
||||
final int highUnit = RenderSystem.getMaxCombinedTextureImageUnits() - 1;
|
||||
final int sampler = RenderSystem.genSampler();
|
||||
try {
|
||||
GLStateManager.glBindSampler(2, sampler);
|
||||
GLStateManager.glBindSampler(highUnit, sampler);
|
||||
GL33.glBindSampler(2, 0);
|
||||
GL33.glBindSampler(highUnit, 0);
|
||||
assertEquals(0, driverBinding(2), "setup failed to diverge the driver");
|
||||
assertEquals(0, driverBinding(highUnit), "setup failed to diverge the driver");
|
||||
|
||||
GLStateManager.replayStateToBackend();
|
||||
|
||||
assertEquals(sampler, driverBinding(2), "replay dropped a cached sampler binding");
|
||||
assertEquals(sampler, driverBinding(highUnit), "replay skipped the highest bound unit");
|
||||
} finally {
|
||||
RenderSystem.destroySampler(sampler);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void sparseBindingsKeepTheCleanupLimitAccurateAfterUnbindAndDeletion() {
|
||||
final int highUnit = RenderSystem.getMaxCombinedTextureImageUnits() - 1;
|
||||
final int low = RenderSystem.genSampler();
|
||||
final int high = RenderSystem.genSampler();
|
||||
try {
|
||||
GLStateManager.glBindSampler(1, low);
|
||||
GLStateManager.glBindSampler(3, high);
|
||||
GLStateManager.glBindSampler(highUnit, high);
|
||||
assertEquals(highUnit, GLStateManager.ctx().samplerUnits.getHighestBoundUnit());
|
||||
|
||||
GLStateManager.glBindSampler(highUnit, 0);
|
||||
assertEquals(3, GLStateManager.ctx().samplerUnits.getHighestBoundUnit());
|
||||
assertEquals(0, driverBinding(highUnit));
|
||||
|
||||
GLStateManager.glBindSampler(highUnit, high);
|
||||
RenderSystem.destroySampler(high);
|
||||
assertEquals(1, GLStateManager.ctx().samplerUnits.getHighestBoundUnit());
|
||||
assertEquals(0, driverBinding(highUnit));
|
||||
assertEquals(0, driverBinding(3));
|
||||
|
||||
RenderSystem.unbindAllSamplers();
|
||||
assertEquals(-1, GLStateManager.ctx().samplerUnits.getHighestBoundUnit());
|
||||
assertEquals(0, driverBinding(1));
|
||||
|
||||
GLStateManager.glBindSampler(highUnit, low);
|
||||
RenderSystem.unbindAllSamplers();
|
||||
RenderSystem.unbindAllSamplers();
|
||||
assertEquals(-1, GLStateManager.ctx().samplerUnits.getHighestBoundUnit());
|
||||
assertEquals(0, driverBinding(highUnit));
|
||||
} finally {
|
||||
RenderSystem.destroySampler(low);
|
||||
RenderSystem.destroySampler(high);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void intBufferGenFillsFromPositionWithBindableSamplers() {
|
||||
final IntBuffer buf = ints(-1, 0, 0);
|
||||
|
||||
-4
@@ -202,7 +202,6 @@ public class GLSM_SharedDrawableContext_GLTest {
|
||||
|
||||
DrawableContexts.run(sharedDrawable, "SharedDrawableContext-Reentry-Thread", () -> {
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
final boolean wasEnabled = ShaderManager.isEnabled();
|
||||
ShaderManager.enable();
|
||||
sm.activate();
|
||||
try {
|
||||
@@ -228,7 +227,6 @@ public class GLSM_SharedDrawableContext_GLTest {
|
||||
GLStateManager.glDeleteBuffers(vbo);
|
||||
} finally {
|
||||
sm.deactivate();
|
||||
if (!wasEnabled) ShaderManager.disable();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -273,7 +271,6 @@ public class GLSM_SharedDrawableContext_GLTest {
|
||||
void indexedListCompiledOnSharedDrawableReplaysOnDisplay() throws Exception {
|
||||
final int list = compileIndexedListOnSharedDrawable();
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
final boolean wasEnabled = ShaderManager.isEnabled();
|
||||
ShaderManager.enable();
|
||||
sm.activate();
|
||||
try {
|
||||
@@ -283,7 +280,6 @@ public class GLSM_SharedDrawableContext_GLTest {
|
||||
assertEquals(indexedBatch().getSharedEBO(), GL11.glGetInteger(GL15.GL_ELEMENT_ARRAY_BUFFER_BINDING));
|
||||
} finally {
|
||||
sm.deactivate();
|
||||
if (!wasEnabled) ShaderManager.disable();
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ class FFPVertexLightingGLTest {
|
||||
GLStateManager.glDisable(GL11.GL_COLOR_MATERIAL);
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
if (sm.isActive()) sm.deactivate();
|
||||
ShaderManager.disable();
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
if (vbo != 0) {
|
||||
GLStateManager.glDeleteBuffers(vbo);
|
||||
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.RenderSystem;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL12;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags.TEXTURE_BIT;
|
||||
import static org.joml.Math.clamp;
|
||||
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.assertTrue;
|
||||
|
||||
@GLCoreTest
|
||||
class TextureSupersamplingGLTest {
|
||||
private static final int SIZE = 200;
|
||||
private int texture;
|
||||
private int vao;
|
||||
private int vbo;
|
||||
|
||||
private static int dotColor(int x, int y) {
|
||||
final boolean dot = (x == 3 || x == 6 || x == 9 || x == 12)
|
||||
&& (y == 3 || y == 6 || y == 9 || y == 12)
|
||||
&& (x == 3 || x == 12 || y == 3 || y == 12);
|
||||
return dot ? 0x444444FF : 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
private static int uploadTexture(int spriteSize, int gutter, IntBinaryOperator color) {
|
||||
final int atlasSize = Integer.highestOneBit(spriteSize + 2 * gutter) << 1;
|
||||
final ByteBuffer pixels = BufferUtils.createByteBuffer(atlasSize * atlasSize * 4);
|
||||
for (int y = 0; y < atlasSize; y++) {
|
||||
for (int x = 0; x < atlasSize; x++) {
|
||||
final int rgba = x < spriteSize + 2 * gutter && y < spriteSize + 2 * gutter
|
||||
? color.applyAsInt(clamp(0, spriteSize - 1, x - gutter), clamp(0, spriteSize - 1, y - gutter))
|
||||
: 0xFF00FFFF;
|
||||
pixels.put((byte) (rgba >>> 24)).put((byte) (rgba >>> 16)).put((byte) (rgba >>> 8)).put((byte) rgba);
|
||||
}
|
||||
}
|
||||
pixels.flip();
|
||||
final int texture = GLStateManager.glGenTextures();
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
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, GL12.GL_TEXTURE_MAX_LEVEL, 0);
|
||||
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, atlasSize, atlasSize, 0,
|
||||
GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static void vertex(ByteBuffer vertices, float x, float z, float u, float v) {
|
||||
vertices.putFloat(x).putFloat(0.5f).putFloat(z).putFloat(u).putFloat(v);
|
||||
}
|
||||
|
||||
private static int countDotRegions(int[] pixels) {
|
||||
final boolean[] remaining = new boolean[pixels.length];
|
||||
for (int i = 0; i < pixels.length; i++) {
|
||||
final int gray = pixels[i] & 255;
|
||||
remaining[i] = gray > 0 && gray < 250
|
||||
&& ((pixels[i] >> 8) & 255) == gray && ((pixels[i] >> 16) & 255) == gray;
|
||||
}
|
||||
int regions = 0;
|
||||
final ArrayDeque<Integer> pending = new ArrayDeque<>();
|
||||
for (int i = 0; i < remaining.length; i++) {
|
||||
if (!remaining[i]) continue;
|
||||
regions++;
|
||||
remaining[i] = false;
|
||||
pending.add(i);
|
||||
while (!pending.isEmpty()) {
|
||||
final int pixel = pending.removeFirst();
|
||||
for (int neighbor : new int[]{pixel - 1, pixel + 1, pixel - SIZE, pixel + SIZE}) {
|
||||
if (neighbor >= 0 && neighbor < remaining.length && remaining[neighbor]) {
|
||||
remaining[neighbor] = false;
|
||||
pending.add(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return regions;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
FfpFixture.resetFfpState();
|
||||
GLStateManager.setTextureSupersamplingSamples(1);
|
||||
GLStateManager.glDisable(GL11.GL_LIGHTING);
|
||||
GLStateManager.glDisable(GL11.GL_FOG);
|
||||
GLStateManager.glColor4f(1, 1, 1, 1);
|
||||
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
|
||||
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
|
||||
GLStateManager.glViewport(0, 0, SIZE, SIZE);
|
||||
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glOrtho(0, SIZE, SIZE, 0, -100, 100);
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
FfpFixture.activateFfp();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
GLStateManager.setTextureSupersamplingSamples(1);
|
||||
FfpFixture.resetFfpState();
|
||||
if (texture != 0) GLStateManager.glDeleteTextures(texture);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
if (vbo != 0) GLStateManager.glDeleteBuffers(vbo);
|
||||
if (vao != 0) GLStateManager.glDeleteVertexArrays(vao);
|
||||
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glViewport(0, 0, 800, 600);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {4, 16})
|
||||
void recoversTwelveDotsAtScaleThreeAndRestoresNearestAfterward(int samples) {
|
||||
createDotAtlas();
|
||||
|
||||
final int[] nearest = drawTop(3);
|
||||
assertEquals(0, countDotRegions(nearest));
|
||||
final int previous = GLStateManager.setTextureSupersamplingSamples(samples);
|
||||
assertEquals(12, countDotRegions(drawTop(3)));
|
||||
GLStateManager.setTextureSupersamplingSamples(previous);
|
||||
assertArrayEquals(nearest, drawTop(3));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {4, 16})
|
||||
void highResolutionCheckerboardsStaySmoothWithTheDefaultAtlasGutter(int samples) {
|
||||
createAtlas(256, 16, (x, y) -> ((x ^ y) & 1) == 0 ? 0xFFFFFFFF : 0x000000FF);
|
||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 4);
|
||||
GLStateManager.glGenerateMipmap(GL11.GL_TEXTURE_2D);
|
||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR_MIPMAP_LINEAR);
|
||||
final int[] filtered = drawFlat();
|
||||
GLStateManager.setTextureSupersamplingSamples(samples);
|
||||
final int[] supersampled = drawFlat();
|
||||
for (int y = 97; y <= 102; y++) {
|
||||
for (int x = 97; x <= 102; x++) {
|
||||
final int index = y * SIZE + x;
|
||||
assertEquals(128, filtered[index] & 255, 1, "The mip-filtered checkerboard must average to gray");
|
||||
assertEquals(filtered[index] & 255, supersampled[index] & 255, 1,
|
||||
"Supersampling must preserve the mip average instead of aliasing the base-level checkerboard");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {4, 16})
|
||||
void transparentTexelsDoNotTintPartialCoverage(int samples) {
|
||||
createAtlas(16, (x, y) -> x < 8 ? 0xFF0000FF : 0x0000FF00);
|
||||
GLStateManager.glEnable(GL11.GL_BLEND);
|
||||
GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
GLStateManager.setTextureSupersamplingSamples(samples);
|
||||
final int[] pixels = drawTop(3);
|
||||
int partialPixels = 0;
|
||||
for (int pixel : pixels) {
|
||||
final int red = pixel & 255;
|
||||
if (red > 0 && red < 255) {
|
||||
partialPixels++;
|
||||
assertEquals(0, (pixel >> 8) & 255);
|
||||
assertEquals(0, (pixel >> 16) & 255, "Transparent blue texels must not tint the red edge");
|
||||
}
|
||||
}
|
||||
assertTrue(partialPixels > 0, "The test must cover a partially transparent pixel");
|
||||
}
|
||||
|
||||
@Test
|
||||
void modelTexturesKeepTheirOwnFilteringWhileSupersamplingIsActive() {
|
||||
createDotAtlas();
|
||||
final int modelTexture = uploadTexture(16, 1, TextureSupersamplingGLTest::dotColor);
|
||||
try {
|
||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
|
||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
|
||||
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[] normal = drawTop(3);
|
||||
GLStateManager.setTextureSupersamplingSamples(16);
|
||||
assertArrayEquals(normal, drawTop(3));
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
|
||||
assertFalse(Arrays.equals(normal, drawTop(3)));
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, modelTexture);
|
||||
assertArrayEquals(normal, drawTop(3));
|
||||
} finally {
|
||||
GLStateManager.glDeleteTextures(modelTexture);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void atlasFetchesIgnoreSamplerFilteringAndRepeatAcrossNegativeUvs() {
|
||||
createAtlas(16, 4, TextureSupersamplingGLTest::dotColor);
|
||||
GLStateManager.setTextureSupersamplingSamples(16);
|
||||
final int[] nearest = drawTop(3);
|
||||
final int sampler = RenderSystem.genSampler();
|
||||
try {
|
||||
RenderSystem.samplerParameteri(sampler, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
|
||||
RenderSystem.samplerParameteri(sampler, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
|
||||
RenderSystem.samplerParameteri(sampler, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
|
||||
RenderSystem.samplerParameteri(sampler, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);
|
||||
RenderSystem.bindSamplerToUnit(0, sampler);
|
||||
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
|
||||
GLStateManager.glTranslatef(-2, 3, 0);
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
assertArrayEquals(nearest, drawTop(3));
|
||||
} finally {
|
||||
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
RenderSystem.bindSamplerToUnit(0, 0);
|
||||
RenderSystem.destroySampler(sampler);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void changingTheAtlasGutterSwitchesBetweenMipFilteringAndFullFootprintSampling() {
|
||||
createAtlas(32, 4, (x, y) -> 0xFF0000FF);
|
||||
final int[] baseLevel = drawFlat();
|
||||
for (int level = 1; level <= 6; level++) {
|
||||
final int size = 64 >> level;
|
||||
final ByteBuffer pixels = BufferUtils.createByteBuffer(size * size * 4);
|
||||
for (int i = 0; i < size * size; i++) pixels.putInt(0xFFFF0000);
|
||||
pixels.flip();
|
||||
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, level, GL11.GL_RGBA8, size, size, 0,
|
||||
GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
|
||||
}
|
||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 6);
|
||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST_MIPMAP_NEAREST);
|
||||
final int[] mipFiltered = drawFlat();
|
||||
assertFalse(Arrays.equals(baseLevel, mipFiltered));
|
||||
GLStateManager.setTextureSupersamplingSamples(16);
|
||||
assertArrayEquals(baseLevel, drawFlat());
|
||||
GLStateManager.markTextureAsAtlas(texture, 1);
|
||||
assertArrayEquals(mipFiltered, drawFlat());
|
||||
GLStateManager.markTextureAsAtlas(texture, 4);
|
||||
assertArrayEquals(baseLevel, drawFlat());
|
||||
}
|
||||
|
||||
private int[] drawFlat() {
|
||||
FfpFixture.clear();
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glTranslatef(100.25f, 100.125f, 0);
|
||||
GLStateManager.glScalef(11, 11, 1);
|
||||
GLStateManager.glRotatef(90, 1, 0, 0);
|
||||
GLStateManager.glDrawArrays(GL11.GL_QUADS, 0, 4);
|
||||
return FfpFixture.readRegion(SIZE);
|
||||
}
|
||||
|
||||
private void createDotAtlas() {
|
||||
createAtlas(16, TextureSupersamplingGLTest::dotColor);
|
||||
}
|
||||
|
||||
private void createAtlas(int spriteSize, IntBinaryOperator color) {
|
||||
createAtlas(spriteSize, 1, color);
|
||||
}
|
||||
|
||||
private void createAtlas(int spriteSize, int gutter, IntBinaryOperator color) {
|
||||
texture = uploadTexture(spriteSize, gutter, color);
|
||||
GLStateManager.markTextureAsAtlas(texture, gutter);
|
||||
final int atlasSize = Integer.highestOneBit(spriteSize + 2 * gutter) << 1;
|
||||
final float minUv = (gutter + 0.01f) / atlasSize;
|
||||
final float maxUv = (spriteSize + gutter - 0.01f) / atlasSize;
|
||||
final ByteBuffer vertices = BufferUtils.createByteBuffer(4 * 20);
|
||||
vertex(vertices, -0.5f, -0.5f, minUv, minUv);
|
||||
vertex(vertices, 0.5f, -0.5f, maxUv, minUv);
|
||||
vertex(vertices, 0.5f, 0.5f, maxUv, maxUv);
|
||||
vertex(vertices, -0.5f, 0.5f, minUv, maxUv);
|
||||
vertices.flip();
|
||||
vao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
vbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, vertices, GL15.GL_STATIC_DRAW);
|
||||
FfpFixture.attrib(0, 3, GL11.GL_FLOAT, false, 20, 0);
|
||||
FfpFixture.attrib(2, 2, GL11.GL_FLOAT, false, 20, 12);
|
||||
VAOManager.setCurrentVertexFlags(TEXTURE_BIT);
|
||||
}
|
||||
|
||||
private int[] drawTop(int scale) {
|
||||
FfpFixture.clear();
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glTranslatef(100 + 8 * scale, 100 + 8 * scale, 0);
|
||||
GLStateManager.glScalef(10 * scale, 10 * scale, -10 * scale);
|
||||
GLStateManager.glRotatef(210, 1, 0, 0);
|
||||
GLStateManager.glRotatef(45, 0, 1, 0);
|
||||
GLStateManager.glDrawArrays(GL11.GL_QUADS, 0, 4);
|
||||
return FfpFixture.readRegion(SIZE);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import org.lwjgl.opengl.GL32;
|
||||
import org.lwjgl.opengl.PixelFormat;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL;
|
||||
|
||||
/**
|
||||
@@ -82,8 +83,11 @@ public class GLSMCoreExtension implements BeforeAllCallback, BeforeEachCallback,
|
||||
public void afterEach(ExtensionContext context) {
|
||||
final int error = GL11.glGetError();
|
||||
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
|
||||
final boolean ffpDisabled = !ShaderManager.isEnabled();
|
||||
if (ffpDisabled) ShaderManager.enable();
|
||||
GlsmRedirectAgent.assertNoFailures();
|
||||
assertEquals(GL11.GL_NO_ERROR, error, () -> "GL Error: 0x" + Integer.toHexString(error));
|
||||
if (ffpDisabled) fail("test left FFP disabled");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.glsm;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.loading.ClassDump;
|
||||
import com.gtnewhorizons.angelica.glsm.loading.ModRedirector;
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.MutableStatics;
|
||||
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
@@ -27,6 +28,7 @@ public final class GlsmRedirectAgent {
|
||||
}
|
||||
|
||||
public static void install(Instrumentation instrumentation) {
|
||||
MutableStatics.install(instrumentation);
|
||||
final ModRedirector redirector = new ModRedirector(ClassDump.DISABLED);
|
||||
addTransformer(instrumentation, (className, classfile) -> {
|
||||
if (!className.startsWith(GTNHLIB)) return null;
|
||||
|
||||
@@ -237,7 +237,6 @@ public final class FfpFixture {
|
||||
GLStateManager.setFfpInstancing(Instancing.NONE);
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
if (sm.isActive()) sm.deactivate();
|
||||
ShaderManager.disable();
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.gtnewhorizons.angelica.glsm.testutil;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GlsmRedirectAgent;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.FieldNode;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/* Test agent: strips ACC_FINAL from listed static finals at load to avoid JIT inlining them when tests want to mutate them */
|
||||
public final class MutableStatics {
|
||||
|
||||
private static final Map<String, Set<String>> FIELDS_BY_OWNER = new HashMap<>();
|
||||
|
||||
static {
|
||||
add("com/gtnewhorizons/angelica/glsm/backend/BackendManager", "RENDER_BACKEND");
|
||||
add("com/gtnewhorizons/angelica/glsm/GLStateManager", "MainThread");
|
||||
add("com/gtnewhorizons/angelica/config/SystemProperties", "UNMAPPED_GL", "SDL_DISABLE_IN_PASS_CLEAR");
|
||||
add("net/coderbot/iris/Iris", "enabled");
|
||||
}
|
||||
|
||||
private MutableStatics() {}
|
||||
|
||||
private static void add(String owner, String... fields) {
|
||||
FIELDS_BY_OWNER.put(owner, new HashSet<>(Arrays.asList(fields)));
|
||||
}
|
||||
|
||||
public static void premain(String args, Instrumentation instrumentation) {
|
||||
install(instrumentation);
|
||||
}
|
||||
|
||||
public static void install(Instrumentation instrumentation) {
|
||||
GlsmRedirectAgent.addTransformer(instrumentation, (className, classfile) -> {
|
||||
final Set<String> fields = FIELDS_BY_OWNER.get(className);
|
||||
return fields == null ? null : stripFinal(classfile, fields);
|
||||
});
|
||||
}
|
||||
|
||||
private static byte[] stripFinal(byte[] classfile, Set<String> fields) {
|
||||
final ClassNode node = new ClassNode();
|
||||
new ClassReader(classfile).accept(node, 0);
|
||||
int patched = 0;
|
||||
for (FieldNode field : node.fields) {
|
||||
if (fields.contains(field.name)) {
|
||||
field.access &= ~Opcodes.ACC_FINAL;
|
||||
patched++;
|
||||
}
|
||||
}
|
||||
if (patched != fields.size()) throw new IllegalStateException(node.name + " lacks one of " + fields);
|
||||
final ClassWriter writer = new ClassWriter(0);
|
||||
node.accept(writer);
|
||||
return writer.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.gtnewhorizons.angelica.glsm.testutil;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.reflect.Fields;
|
||||
import com.gtnewhorizon.gtnhlib.reflect.Fields.LookupType;
|
||||
import com.gtnewhorizons.angelica.glsm.GlsmRedirectAgent;
|
||||
import sun.misc.Unsafe;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
public final class Reflect {
|
||||
|
||||
@@ -59,7 +59,13 @@ public final class Reflect {
|
||||
}
|
||||
|
||||
public static <F> void setStaticFinal(Class<?> owner, String name, Class<F> type, F value) {
|
||||
Fields.ofClass(owner).getField(LookupType.DECLARED_IN_HIERARCHY, name, type).setValue(null, value);
|
||||
final Field f = field(owner, name);
|
||||
if (f.getType() != type) throw new AssertionError(owner.getName() + "." + name + " is " + f.getType().getName() + ", not " + type.getName());
|
||||
if (Modifier.isFinal(f.getModifiers())) {
|
||||
GlsmRedirectAgent.assertNoFailures();
|
||||
throw new AssertionError(owner.getName() + "." + name + " is still final; add it to MutableStatics");
|
||||
}
|
||||
setStatic(owner, name, value);
|
||||
}
|
||||
|
||||
public static void setDeclared(Class<?> owner, Object target, String name, Object value) {
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
# ALSO_JFR=1 - with a .tracy output, also record a JFR alongside it (timings inflated, correlation only).
|
||||
# ASPROF_OPTS - comma separated async-profiler agent opts, passed through when a JFR is recorded.
|
||||
#
|
||||
# FLYBY_FULLSCREEN=0 - keep options.txt's window mode. Default: fullscreen:true for the run (avoids vsync pacing), restored on exit.
|
||||
#
|
||||
# The output extension selects the profiler: .tracy for Tracy, .jfr for an in-process async-profiler
|
||||
# JFR that starts near the end of warmup and stops when the measured run ends.
|
||||
#
|
||||
@@ -32,6 +34,9 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=flyby-lib.sh
|
||||
. "$(dirname "$0")/flyby-lib.sh"
|
||||
|
||||
OUT="${1:?usage: flyby-capture.sh <output.tracy|output.jfr> [route] [length] [extra gradle args...]}"
|
||||
ROUTE="${2:-straight}"
|
||||
LENGTH="${3:-0}"
|
||||
@@ -148,12 +153,40 @@ if [ "$MODE" = "tracy" ]; then
|
||||
if kill -0 "$CAPTURE_PID" 2>/dev/null; then
|
||||
echo "==> stopping capture"
|
||||
kill -INT "$CAPTURE_PID" 2>/dev/null || true
|
||||
local waited=0
|
||||
while kill -0 "$CAPTURE_PID" 2>/dev/null && { { [ -e "$OUT" ] && [ "$waited" -lt 600 ]; } || [ "$waited" -lt 120 ]; }; do
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
if kill -0 "$CAPTURE_PID" 2>/dev/null; then
|
||||
echo "==> capture did not stop after ${waited}s (limit 120s before output exists, 600s after), killing it" >&2
|
||||
kill -9 "$CAPTURE_PID" 2>/dev/null || true
|
||||
fi
|
||||
wait "$CAPTURE_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
fi
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
OPTIONS_TXT="$ROOT/run/client/options.txt"
|
||||
FS_DIR="$(mktemp -d)"
|
||||
|
||||
on_exit() {
|
||||
if [ -n "$CAPTURE_PID" ]; then
|
||||
cleanup
|
||||
fi
|
||||
if [ "$FULLSCREEN" != 0 ]; then
|
||||
flyby_fullscreen_restore "$OPTIONS_TXT" "$FS_DIR/options.txt.bak" || true
|
||||
fi
|
||||
rm -rf "$FS_DIR"
|
||||
}
|
||||
FULLSCREEN="${FLYBY_FULLSCREEN:-1}"
|
||||
if [ "$FULLSCREEN" != 0 ]; then
|
||||
flyby_fullscreen_on "$OPTIONS_TXT" "$FS_DIR/options.txt.bak"
|
||||
fi
|
||||
trap on_exit EXIT
|
||||
|
||||
echo "==> launching game (route=$ROUTE warmup=$WARMUP length=${LENGTH:-default} scene=${SCENE_ABS:-none})"
|
||||
if [ -n "$JFR_OUT" ]; then
|
||||
echo "==> recording JFR -> $JFR_ABS"
|
||||
@@ -178,7 +211,6 @@ fi
|
||||
echo "==> game exited, finalising capture"
|
||||
if [ "$MODE" = "tracy" ]; then
|
||||
cleanup
|
||||
trap - EXIT
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "jfr" ]; then
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared file helpers for the flyby-*.sh scripts. Sourced, not executed: . "$(dirname "$0")/flyby-lib.sh"
|
||||
#
|
||||
# flyby_set_key <file> <key> <value> [sep] replace the first line starting with <key><sep> (default sep ":"), else append; creates the file
|
||||
# flyby_copy <src> <dst> copy bytes and verify with a compare
|
||||
# flyby_backup_file <file> <backup> copy to <backup>; if <file> is absent, remove any stale <backup> (records absence)
|
||||
# flyby_restore_file <backup> <file> flyby_backup_file with swapped arguments; prints one error line and returns 1 on failure.
|
||||
# Trap callers append '|| true'. Removes <file> if <backup> is absent: only call it after the backup was made.
|
||||
# flyby_fullscreen_on <options.txt> <backup> backs up, sets fullscreen:true
|
||||
# flyby_fullscreen_restore <options.txt> <backup> restores the backup; idempotent, call only after flyby_fullscreen_on
|
||||
# flyby_fresh_dir <dir> remove <dir> and recreate it empty
|
||||
# flyby_collect <run_dir> <variant_dir> <log>... copy <run_dir>/logs/<log> and <run_dir>/screenshots/flyby-shot-*.png into <variant_dir>
|
||||
# flyby_crash_list <dir> sorted names in <dir>, nothing if it is absent
|
||||
# flyby_new_crashes <dir> <before> <after> write the current crash list to <after>, print how many names are not in <before>
|
||||
|
||||
flyby_set_key() {
|
||||
python3 - "$1" "$2" "$3" "${4:-:}" <<'PY'
|
||||
import os, re, sys
|
||||
path, key, value, sep = sys.argv[1:5]
|
||||
text = ""
|
||||
if os.path.isfile(path):
|
||||
with open(path, newline="") as f:
|
||||
text = f.read()
|
||||
pat = re.compile(r"^" + re.escape(key + sep) + r"[^\r\n]*", re.M)
|
||||
line = key + sep + value
|
||||
if pat.search(text):
|
||||
text = pat.sub(lambda _: line, text, count=1)
|
||||
else:
|
||||
if text and not text.endswith("\n"):
|
||||
text += "\n"
|
||||
text += line + "\n"
|
||||
with open(path, "w", newline="") as f:
|
||||
f.write(text)
|
||||
PY
|
||||
}
|
||||
|
||||
flyby_copy() {
|
||||
python3 - "$1" "$2" <<'PY'
|
||||
import filecmp, shutil, sys
|
||||
src, dst = sys.argv[1:3]
|
||||
shutil.copyfile(src, dst)
|
||||
if not filecmp.cmp(src, dst, shallow=False):
|
||||
sys.exit("error: copy differs: " + dst)
|
||||
PY
|
||||
}
|
||||
|
||||
flyby_backup_file() {
|
||||
python3 - "$1" "$2" <<'PY'
|
||||
import filecmp, os, shutil, sys
|
||||
path, backup = sys.argv[1:3]
|
||||
if os.path.isfile(path):
|
||||
shutil.copyfile(path, backup)
|
||||
if not filecmp.cmp(path, backup, shallow=False):
|
||||
sys.exit("error: backup differs: " + backup)
|
||||
elif os.path.isfile(backup):
|
||||
os.remove(backup)
|
||||
PY
|
||||
}
|
||||
|
||||
flyby_restore_file() {
|
||||
flyby_backup_file "$1" "$2" || { echo "error: restore failed or differs: $2" >&2; return 1; }
|
||||
}
|
||||
|
||||
flyby_fullscreen_on() {
|
||||
flyby_backup_file "$1" "$2"
|
||||
flyby_set_key "$1" fullscreen true :
|
||||
}
|
||||
|
||||
flyby_fullscreen_restore() {
|
||||
flyby_restore_file "$2" "$1"
|
||||
}
|
||||
|
||||
flyby_fresh_dir() {
|
||||
python3 -c 'import shutil, sys; shutil.rmtree(sys.argv[1], ignore_errors=True)' "$1"
|
||||
mkdir -p "$1"
|
||||
}
|
||||
|
||||
flyby_collect() {
|
||||
python3 - "$@" <<'PY'
|
||||
import glob, os, shutil, sys
|
||||
run, vdir = sys.argv[1:3]
|
||||
for name in sys.argv[3:]:
|
||||
src = os.path.join(run, "logs", name)
|
||||
if os.path.isfile(src):
|
||||
shutil.copyfile(src, os.path.join(vdir, name))
|
||||
for src in glob.glob(os.path.join(run, "screenshots", "flyby-shot-*.png")):
|
||||
shutil.copyfile(src, os.path.join(vdir, os.path.basename(src)))
|
||||
PY
|
||||
}
|
||||
|
||||
flyby_crash_list() {
|
||||
if [ -d "$1" ]; then
|
||||
ls -1 "$1" | sort
|
||||
fi
|
||||
}
|
||||
|
||||
flyby_new_crashes() {
|
||||
flyby_crash_list "$1" >| "$3"
|
||||
comm -13 "$2" "$3" | wc -l | tr -d ' '
|
||||
}
|
||||
Executable
+480
@@ -0,0 +1,480 @@
|
||||
#!/usr/bin/env bash
|
||||
# Unattended flyby run of a test Angelica jar inside the real Prism GTNH instance, one launch per variant
|
||||
#
|
||||
# Usage: scripts/flyby-prism.sh <name> <variant>...
|
||||
# Variants: sdl-off | sdl-pack | gl-off | gl-pack (SDL-GPU vs OpenGL x shader pack off vs on)
|
||||
# gl-pack writes <pack>.txt from the pack's profile.HIGH first (never Ultra on OpenGL) and fails before launching without it.
|
||||
# Output: tmp/prism/<name>/<variant>/ (fml-client-latest.log, flyby-shot-*.png, console from the checker).
|
||||
# Prints one PASS/FAIL line per variant; exits non-zero if any fails. A run that never reaches the world
|
||||
# (no 'Flyby: world saving is off' in the log) is FAILED and stops the batch.
|
||||
# Needs a hands-off machine: the game must keep focus (waitForFocus) and runs fullscreen.
|
||||
#
|
||||
# PRISM_INSTANCE - instance id (default: GTNH-daily-2026-10-01+766-mmcprism-java17-26).
|
||||
# PRISM_BIN - launcher binary (default: /Applications/Prism Launcher.app/Contents/MacOS/prismlauncher).
|
||||
# PRISM_TIMEOUT - seconds before a variant's game is killed (default: 900).
|
||||
# PRISM_KEEP_JAR - 1 leaves the test jar installed and the old angelica jar(s) disabled after the script ends (default: 0).
|
||||
# PRISM_WORLD - save for QuickPlay (default: FlybyGTNH). No spaces.
|
||||
# PRISM_ENTRIES - id=status,... passed as -Dangelica.flyby.debugEntries on every variant and graded with
|
||||
# --require-renderers (default: all nine world renderers inOverlay).
|
||||
#
|
||||
# Sets fullscreen:true in the instance options.txt per variant (avoids vsync pacing).
|
||||
# Restores instance.cfg, options.txt, config/shaders.properties and the pack option file on exit (cmp-verified).
|
||||
# scripts/flyby-prism.sh --helper <jvmargs|profile> ... runs one pure step in isolation.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=flyby-lib.sh
|
||||
. "$(dirname "$0")/flyby-lib.sh"
|
||||
|
||||
HELPER="$(cat <<'PY'
|
||||
import re, sys
|
||||
|
||||
|
||||
def read(path):
|
||||
with open(path, newline="") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def write(path, text):
|
||||
with open(path, "w", newline="") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
def unquote(raw):
|
||||
if len(raw) >= 2 and raw[0] == '"' and raw[-1] == '"':
|
||||
return True, raw[1:-1].replace('\\"', '"').replace("\\\\", "\\")
|
||||
return False, raw
|
||||
|
||||
|
||||
def requote(value, quoted):
|
||||
if quoted or re.search(r"[\s,;\"]", value):
|
||||
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
return value
|
||||
|
||||
|
||||
def key_of(token):
|
||||
return token.split("=", 1)[0]
|
||||
|
||||
|
||||
def jvmargs(cfg, original, backend, extra):
|
||||
src = read(original)
|
||||
m = re.search(r"^JvmArgs=([^\r\n]*)", src, re.M)
|
||||
quoted, value = unquote(m.group(1)) if m else (True, "")
|
||||
new = ["-Dangelica.sdlgpu.enable=" + backend] + extra
|
||||
for t in new:
|
||||
if re.search(r"\s", t) or '"' in t:
|
||||
raise SystemExit("error: jvm arg contains whitespace or a quote: " + repr(t))
|
||||
drop = {key_of(t) for t in new}
|
||||
kept = [t for t in value.split() if key_of(t) not in drop]
|
||||
line = "JvmArgs=" + requote(" ".join(kept + new), quoted)
|
||||
text = read(cfg)
|
||||
if re.search(r"^JvmArgs=[^\r\n]*", text, re.M):
|
||||
text = re.sub(r"^JvmArgs=[^\r\n]*", lambda _: line, text, count=1, flags=re.M)
|
||||
elif re.search(r"^\[General\][^\r\n]*", text, re.M):
|
||||
text = re.sub(r"^(\[General\][^\r\n]*)(\r?\n)", lambda g: g.group(1) + g.group(2) + line + g.group(2), text, count=1, flags=re.M)
|
||||
else:
|
||||
text = line + "\n" + text
|
||||
if re.search(r"^OverrideJavaArgs=[^\r\n]*", text, re.M):
|
||||
text = re.sub(r"^OverrideJavaArgs=[^\r\n]*", "OverrideJavaArgs=true", text, count=1, flags=re.M)
|
||||
else:
|
||||
text = re.sub(r"^(JvmArgs=[^\r\n]*)(\r?\n|$)", lambda g: g.group(1) + (g.group(2) or "\n") + "OverrideJavaArgs=true" + g.group(2), text, count=1, flags=re.M)
|
||||
write(cfg, text)
|
||||
print(line)
|
||||
|
||||
|
||||
def logical_lines(text):
|
||||
out = []
|
||||
buf = ""
|
||||
for raw in text.splitlines():
|
||||
if buf:
|
||||
piece = raw.strip()
|
||||
else:
|
||||
piece = raw.rstrip()
|
||||
if piece.endswith("\\"):
|
||||
buf += piece[:-1] + " "
|
||||
continue
|
||||
out.append(buf + piece)
|
||||
buf = ""
|
||||
if buf:
|
||||
out.append(buf)
|
||||
return out
|
||||
|
||||
|
||||
def profile_tokens(defs, name, seen):
|
||||
if name in seen:
|
||||
raise SystemExit("error: profile cycle at " + name)
|
||||
if name not in defs:
|
||||
raise SystemExit("error: profile." + name + " not defined")
|
||||
out = []
|
||||
for tok in defs[name].split():
|
||||
if tok.startswith("profile."):
|
||||
out.extend(profile_tokens(defs, tok[len("profile."):], seen | {name}))
|
||||
else:
|
||||
out.append(tok)
|
||||
return out
|
||||
|
||||
|
||||
def profile(props, out_txt, name):
|
||||
defs = {}
|
||||
for line in logical_lines(read(props)):
|
||||
s = line.strip()
|
||||
if s.startswith("#") or s.startswith("!"):
|
||||
continue
|
||||
m = re.match(r"profile\.(\w+)\s*=(.*)$", s)
|
||||
if m:
|
||||
defs[m.group(1)] = m.group(2)
|
||||
if name not in defs:
|
||||
raise SystemExit("error: profile." + name + " not found in " + props)
|
||||
options = {}
|
||||
for tok in profile_tokens(defs, name, frozenset()):
|
||||
if "=" in tok:
|
||||
k, v = tok.split("=", 1)
|
||||
elif tok.startswith("!"):
|
||||
k, v = tok[1:], "false"
|
||||
else:
|
||||
k, v = tok, "true"
|
||||
if not re.match(r"^[A-Za-z_][A-Za-z0-9_.]*$", k) or v == "":
|
||||
raise SystemExit("error: cannot parse profile token " + repr(tok))
|
||||
options[k] = v
|
||||
if not options:
|
||||
raise SystemExit("error: profile." + name + " is empty")
|
||||
write(out_txt, "".join(k + "=" + v + "\n" for k, v in options.items()))
|
||||
print(len(options))
|
||||
|
||||
|
||||
def main(argv):
|
||||
cmd = argv[1]
|
||||
if cmd == "jvmargs":
|
||||
jvmargs(argv[2], argv[3], argv[4], argv[5:])
|
||||
elif cmd == "profile":
|
||||
profile(argv[2], argv[3], argv[4] if len(argv) > 4 else "HIGH")
|
||||
else:
|
||||
raise SystemExit("error: unknown helper " + cmd)
|
||||
|
||||
|
||||
main(sys.argv)
|
||||
PY
|
||||
)"
|
||||
|
||||
helper() {
|
||||
python3 -c "$HELPER" "$@"
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "--helper" ]; then
|
||||
shift
|
||||
helper "$@"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
NAME="${1:?usage: flyby-prism.sh <name> <variant>...}"
|
||||
shift
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "usage: flyby-prism.sh <name> <sdl-off|sdl-pack|gl-off|gl-pack>..." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
PRISM_INSTANCE="${PRISM_INSTANCE:-GTNH-daily-2026-10-01+766-mmcprism-java17-26}"
|
||||
PRISM_BIN="${PRISM_BIN:-/Applications/Prism Launcher.app/Contents/MacOS/prismlauncher}"
|
||||
PRISM_TIMEOUT="${PRISM_TIMEOUT:-900}"
|
||||
PRISM_KEEP_JAR="${PRISM_KEEP_JAR:-0}"
|
||||
PRISM_WORLD="${PRISM_WORLD:-FlybyGTNH}"
|
||||
PRISM_ENTRIES="${PRISM_ENTRIES:-minecraft:chunk_borders=inOverlay,minecraft:chunk_section_visibility=inOverlay,minecraft:visualize_block_light_levels=inOverlay,minecraft:visualize_sky_light_levels=inOverlay,minecraft:visualize_heightmap=inOverlay,minecraft:visualize_collision_boxes=inOverlay,minecraft:visualize_solid_faces=inOverlay,minecraft:visualize_water_levels=inOverlay,minecraft:visualize_entity_supporting_block=inOverlay}"
|
||||
|
||||
for v in "$@"; do
|
||||
case "$v" in
|
||||
sdl-off|sdl-pack|gl-off|gl-pack) ;;
|
||||
*) echo "error: variant must be sdl-off, sdl-pack, gl-off or gl-pack, got: $v" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
case "$PRISM_WORLD" in
|
||||
*[[:space:]\"]*|"") echo "error: PRISM_WORLD must be non-empty without spaces or quotes: '$PRISM_WORLD'" >&2; exit 2 ;;
|
||||
esac
|
||||
case "$PRISM_ENTRIES" in
|
||||
*[[:space:]\"]*) echo "error: PRISM_ENTRIES must not contain spaces or quotes" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
INST="$HOME/Library/Application Support/PrismLauncher/instances/$PRISM_INSTANCE"
|
||||
MC="$INST/.minecraft"
|
||||
CFG="$INST/instance.cfg"
|
||||
OPTS="$MC/options.txt"
|
||||
SHADERS="$MC/config/shaders.properties"
|
||||
for f in "$CFG" "$OPTS" "$SHADERS"; do
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "error: missing $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if [ ! -x "$PRISM_BIN" ]; then
|
||||
echo "error: launcher not executable: $PRISM_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INST_TAIL="${PRISM_INSTANCE#*+}"
|
||||
GAME_PAT="java.*$INST_TAIL"
|
||||
if pgrep -x prismlauncher >/dev/null 2>&1; then
|
||||
echo "error: Prism Launcher is already running; quit it first" >&2
|
||||
exit 1
|
||||
fi
|
||||
if pgrep -f "$GAME_PAT" >/dev/null 2>&1; then
|
||||
echo "error: a process matching '$GAME_PAT' (the instance's game?) is running" >&2
|
||||
exit 1
|
||||
fi
|
||||
if pgrep -f 'GradleStart|retrofuturabootstrap' >/dev/null 2>&1; then
|
||||
echo "error: a dev client is running" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
./gradlew assemble
|
||||
JAR=""
|
||||
for f in "$ROOT"/build/libs/angelica-*.jar; do
|
||||
case "$f" in
|
||||
*-dev.jar|*-sources.jar|*-api.jar|*-dev-preshadow.jar|*-predowngrade.jar|*shadow-stripped.jar) continue ;;
|
||||
esac
|
||||
if [ -f "$f" ] && { [ -z "$JAR" ] || [ "$f" -nt "$JAR" ]; }; then
|
||||
JAR="$f"
|
||||
fi
|
||||
done
|
||||
if [ -z "$JAR" ]; then
|
||||
echo "error: no production angelica jar in build/libs" >&2
|
||||
exit 1
|
||||
fi
|
||||
JAR_NAME="$(basename "$JAR")"
|
||||
echo "==> test jar: $JAR"
|
||||
|
||||
OUT="$ROOT/tmp/prism/$NAME"
|
||||
BAK="$OUT/backup"
|
||||
|
||||
PACK="$(awk '/^shaderPack=/ { sub(/^[^=]*=/, ""); sub(/\r$/, ""); print; exit }' "$SHADERS")"
|
||||
PACK_TXT=""
|
||||
if [ -n "$PACK" ]; then
|
||||
PACK_TXT="$MC/shaderpacks/$PACK.txt"
|
||||
fi
|
||||
for v in "$@"; do
|
||||
if [ "$v" = "gl-pack" ] || [ "$v" = "sdl-pack" ]; then
|
||||
if [ -z "$PACK" ] || [ ! -d "$MC/shaderpacks/$PACK" ]; then
|
||||
echo "error: shaderPack '$PACK' missing from $MC/shaderpacks" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
STALE_BAK=""
|
||||
if [ -d "$BAK" ]; then
|
||||
STALE_BAK="$(find "$BAK" -mindepth 1 -maxdepth 1 | head -n 1)"
|
||||
fi
|
||||
STALE_DISABLED=""
|
||||
for f in "$MC"/mods/angelica-*.jar.disabled; do
|
||||
if [ -e "$f" ]; then
|
||||
STALE_DISABLED="$STALE_DISABLED $f"
|
||||
fi
|
||||
done
|
||||
if [ -n "$STALE_BAK" ] || [ -n "$STALE_DISABLED" ]; then
|
||||
echo "error: leftovers from an interrupted run; nothing was touched" >&2
|
||||
echo "undo steps:" >&2
|
||||
echo " 1. copy back from $BAK (if present): instance.cfg -> $CFG, options.txt -> $OPTS, shaders.properties -> $SHADERS, pack.txt -> ${PACK_TXT:-n/a}" >&2
|
||||
echo " 2. remove the installed test jar from $MC/mods (originals are listed in $BAK/renamed.list)" >&2
|
||||
echo " 3. drop the .disabled suffix from:${STALE_DISABLED:- (none found)}" >&2
|
||||
echo " 4. rm -rf '$BAK'" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$BAK"
|
||||
|
||||
flyby_copy "$CFG" "$BAK/instance.cfg"
|
||||
flyby_copy "$OPTS" "$BAK/options.txt"
|
||||
flyby_copy "$SHADERS" "$BAK/shaders.properties"
|
||||
if [ -n "$PACK_TXT" ]; then
|
||||
flyby_backup_file "$PACK_TXT" "$BAK/pack.txt"
|
||||
fi
|
||||
|
||||
RENAMED="$BAK/renamed.list"
|
||||
: >| "$RENAMED"
|
||||
|
||||
PRISM_PID=""
|
||||
GAME_PID=""
|
||||
|
||||
kill_game() {
|
||||
local pids
|
||||
pids="$(pgrep -f "$GAME_PAT" || true)"
|
||||
if [ -n "$pids" ]; then
|
||||
# shellcheck disable=SC2086
|
||||
kill $pids 2>/dev/null || true
|
||||
sleep 3
|
||||
pids="$(pgrep -f "$GAME_PAT" || true)"
|
||||
if [ -n "$pids" ]; then
|
||||
# shellcheck disable=SC2086
|
||||
kill -9 $pids 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
if [ -n "$PRISM_PID" ] && kill -0 "$PRISM_PID" 2>/dev/null; then
|
||||
kill "$PRISM_PID" 2>/dev/null || true
|
||||
sleep 1
|
||||
kill -9 "$PRISM_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
RESTORED=0
|
||||
restore() {
|
||||
set +e
|
||||
if [ "$RESTORED" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
RESTORED=1
|
||||
trap - EXIT INT TERM HUP
|
||||
kill_game
|
||||
local ok=1
|
||||
flyby_restore_file "$BAK/instance.cfg" "$CFG" || ok=0
|
||||
flyby_restore_file "$BAK/options.txt" "$OPTS" || ok=0
|
||||
flyby_restore_file "$BAK/shaders.properties" "$SHADERS" || ok=0
|
||||
if [ -n "$PACK_TXT" ]; then
|
||||
flyby_restore_file "$BAK/pack.txt" "$PACK_TXT" || ok=0
|
||||
fi
|
||||
if [ "$PRISM_KEEP_JAR" != "1" ]; then
|
||||
rm -f "$MC/mods/$JAR_NAME" || { echo "warning: could not remove $MC/mods/$JAR_NAME" >&2; ok=0; }
|
||||
while IFS= read -r f; do
|
||||
if [ -n "$f" ] && [ -f "$f.disabled" ]; then
|
||||
mv -f "$f.disabled" "$f" || { echo "warning: could not re-enable $f" >&2; ok=0; }
|
||||
fi
|
||||
done < "$RENAMED"
|
||||
if [ "$ok" = "1" ]; then
|
||||
rm -rf "$BAK"
|
||||
fi
|
||||
echo "==> test jar removed, original angelica jar(s) re-enabled"
|
||||
else
|
||||
echo "==> test jar LEFT installed: $MC/mods/$JAR_NAME"
|
||||
echo "==> to undo: rm -f '$MC/mods/$JAR_NAME'; then drop the .disabled suffix from: $(tr '\n' ' ' < "$RENAMED")"
|
||||
fi
|
||||
}
|
||||
trap restore EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
trap 'exit 129' HUP
|
||||
|
||||
for f in "$MC"/mods/angelica-*.jar; do
|
||||
if [ -f "$f" ]; then
|
||||
printf '%s\n' "$f" >> "$RENAMED"
|
||||
mv -n "$f" "$f.disabled"
|
||||
fi
|
||||
done
|
||||
flyby_copy "$JAR" "$MC/mods/$JAR_NAME"
|
||||
echo "==> installed $JAR_NAME; disabled: $(tr '\n' ' ' < "$RENAMED")"
|
||||
|
||||
game_pid() {
|
||||
pgrep -f "$GAME_PAT" | head -n 1 || true
|
||||
}
|
||||
|
||||
FAILED=0
|
||||
for VARIANT in "$@"; do
|
||||
VDIR="$OUT/$VARIANT"
|
||||
flyby_fresh_dir "$VDIR"
|
||||
|
||||
case "$VARIANT" in
|
||||
sdl-*) BACKEND=true ;;
|
||||
*) BACKEND=false ;;
|
||||
esac
|
||||
case "$VARIANT" in
|
||||
*-pack) SHADERS_ON=true ;;
|
||||
*) SHADERS_ON=false ;;
|
||||
esac
|
||||
|
||||
EXTRA=(
|
||||
-Dangelica.debug.tooling=true
|
||||
-Dangelica.flyby.route=static
|
||||
-Dangelica.flyby.length=400
|
||||
-Dangelica.flyby.warmupTicks=200
|
||||
-Dangelica.flyby.exitWhenDone=true
|
||||
-Dangelica.flyby.waitForFocus=true
|
||||
-Dangelica.flyby.screenshots=4
|
||||
-Dangelica.flyby.debugHud=true
|
||||
-Dangelica.flyby.debugDump=true
|
||||
"-DquickPlaySingleplayer=$PRISM_WORLD"
|
||||
)
|
||||
CHECK=(--modpack --shots 4 --expect-mode OVERLAY --require-group botania:botania --require-group journeymap:journeymap --forbid-dup --require-mod etfuturum)
|
||||
EXTRA+=("-Dangelica.flyby.debugEntries=$PRISM_ENTRIES")
|
||||
RENDERERS="$(printf '%s\n' "$PRISM_ENTRIES" | tr ',' '\n' | sed -e '/^$/d' -e 's/=[^=]*$//' | paste -sd, -)"
|
||||
CHECK+=(--require-renderers "$RENDERERS")
|
||||
|
||||
flyby_restore_file "$BAK/instance.cfg" "$CFG"
|
||||
echo "==> $NAME/$VARIANT: JvmArgs"
|
||||
helper jvmargs "$CFG" "$BAK/instance.cfg" "$BACKEND" "${EXTRA[@]}" | tee "$VDIR/jvmargs.txt"
|
||||
|
||||
flyby_restore_file "$BAK/shaders.properties" "$SHADERS"
|
||||
flyby_set_key "$SHADERS" enableShaders "$SHADERS_ON" =
|
||||
|
||||
if [ -n "$PACK_TXT" ]; then
|
||||
flyby_restore_file "$BAK/pack.txt" "$PACK_TXT"
|
||||
fi
|
||||
if [ "$VARIANT" = "gl-pack" ]; then
|
||||
PROPS="$MC/shaderpacks/$PACK/shaders/shaders.properties"
|
||||
if [ ! -f "$PROPS" ] || ! COUNT="$(helper profile "$PROPS" "$PACK_TXT" HIGH)"; then
|
||||
echo "FAIL $VARIANT: pack profile.HIGH could not be read from $PROPS; not launching"
|
||||
FAILED=1
|
||||
continue
|
||||
fi
|
||||
echo "==> $NAME/$VARIANT: wrote $COUNT HIGH options to $PACK_TXT"
|
||||
fi
|
||||
|
||||
flyby_restore_file "$BAK/options.txt" "$OPTS"
|
||||
flyby_set_key "$OPTS" fullscreen true :
|
||||
|
||||
mkdir -p "$MC/screenshots"
|
||||
rm -f "$MC"/screenshots/flyby-shot-*.png "$MC/logs/fml-client-latest.log"
|
||||
flyby_crash_list "$MC/crash-reports" >| "$VDIR/crashes.before"
|
||||
|
||||
echo "==> $NAME/$VARIANT: launching (timeout ${PRISM_TIMEOUT}s)"
|
||||
"$PRISM_BIN" -l "$PRISM_INSTANCE" >| "$VDIR/launcher.log" 2>&1 &
|
||||
PRISM_PID=$!
|
||||
|
||||
GAME_PID=""
|
||||
WAITED=0
|
||||
while [ "$WAITED" -lt 240 ]; do
|
||||
GAME_PID="$(game_pid)"
|
||||
if [ -n "$GAME_PID" ]; then
|
||||
break
|
||||
fi
|
||||
if ! kill -0 "$PRISM_PID" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
WAITED=$((WAITED + 2))
|
||||
done
|
||||
|
||||
STATUS="0"
|
||||
if [ -z "$GAME_PID" ]; then
|
||||
STATUS="nogame"
|
||||
echo "==> $NAME/$VARIANT: no game process appeared within ${WAITED}s"
|
||||
else
|
||||
DEADLINE=$(( $(date +%s) + PRISM_TIMEOUT ))
|
||||
while kill -0 "$GAME_PID" 2>/dev/null; do
|
||||
if [ "$(date +%s)" -ge "$DEADLINE" ]; then
|
||||
echo "==> $NAME/$VARIANT: timeout, killing game"
|
||||
STATUS="timeout"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
fi
|
||||
kill_game
|
||||
PRISM_PID=""
|
||||
sleep 2
|
||||
|
||||
NEW_CRASHES="$(flyby_new_crashes "$MC/crash-reports" "$VDIR/crashes.before" "$VDIR/crashes.after")"
|
||||
flyby_collect "$MC" "$VDIR" fml-client-latest.log
|
||||
|
||||
if ! grep -qF "Flyby: world saving is off" "$VDIR/fml-client-latest.log" 2>/dev/null; then
|
||||
echo "FAIL $VARIANT: never entered the world (no 'Flyby: world saving is off' in the log; QuickPlay did not fire?); stopping the batch"
|
||||
FAILED=1
|
||||
break
|
||||
fi
|
||||
|
||||
if [ "$STATUS" = "nogame" ]; then
|
||||
STATUS="1"
|
||||
fi
|
||||
if ! python3 "$ROOT/scripts/flyby-smoke-check.py" "$VDIR" "$STATUS" "$NEW_CRASHES" "${CHECK[@]}"; then
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
|
||||
exit "$FAILED"
|
||||
+117
-11
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
@@ -15,8 +16,18 @@ CATCH_MARK = "Caught an exception during ISBRH rendering"
|
||||
BAD_LINE = re.compile(
|
||||
r"InvalidInjectionException|InjectionError|MixinApplyError|MixinTransformerError|"
|
||||
r"InvalidMixinException|MixinPrepareError|skipped: the surrounded call was removed|"
|
||||
r"flyby scene command did not execute"
|
||||
r"flyby scene command did not execute|Grouped debug screen failed|Failed to set up the debug screen|"
|
||||
r"Debug screen entry \S+ failed and is disabled|needs the batching font renderer|"
|
||||
r"Flyby debugEntries:|Flyby: debug entry list is not available|Flyby: unknown debugProfile|"
|
||||
r"Debug screen compat entry for \S+ failed to register|"
|
||||
r"Failed to (?:load|save|set aside the unreadable) debug screen profile|toggle listeners kept changing"
|
||||
)
|
||||
OVERLAY_RE = re.compile(r"Flyby debug overlay shot=(\d+) mode=(\w+)(?:.* renderers=(\S*))?")
|
||||
LINE_RE = re.compile(r"Flyby debug line shot=(\d+) id=(\S+) text=(.*)")
|
||||
GROUP_RE = re.compile(r"Flyby debug group shot=(\d+) id=(\S+) ")
|
||||
DUP_IDS = ("angelica:other_left", "angelica:other_right")
|
||||
DUP_PREFIXES = ("Allocation rate:", "Java:", "GPU:", "OpenGL:", "CPU Cores:", "OS:", "LWJGL:", "IntCache:",
|
||||
"[Botania]", "[JM]", "[ChunkGen]", "hunger:", "HydroEnergy GPU RAM:", "renderDebugMode:")
|
||||
ANGELICA_FRAME = re.compile(
|
||||
r"com\.gtnewhorizons\.angelica|net\.coderbot|org\.embeddedt|org\.taumc|com\.prupe|"
|
||||
r"jss\.notfine|\$surround|angelica\$"
|
||||
@@ -44,7 +55,44 @@ def split_traces(lines):
|
||||
return traces
|
||||
|
||||
|
||||
def check(variant_dir, exit_status, new_crashes):
|
||||
def check_overlay(lines, opts, reasons):
|
||||
overlays = [m for m in (OVERLAY_RE.search(l) for l in lines) if m]
|
||||
mode = opts.get("expect_mode")
|
||||
if mode:
|
||||
if not overlays:
|
||||
reasons.append("no 'Flyby debug overlay' line")
|
||||
else:
|
||||
wrong = sorted({m.group(2) for m in overlays if m.group(2) != mode})
|
||||
if wrong:
|
||||
reasons.append("overlay mode " + ",".join(wrong) + " != expected " + mode)
|
||||
|
||||
dbg_lines = [m for m in (LINE_RE.search(l) for l in lines) if m]
|
||||
groups = [m for m in (GROUP_RE.search(l) for l in lines) if m]
|
||||
for ident in opts.get("require_group") or []:
|
||||
if not any(m.group(2) == ident for m in groups):
|
||||
reasons.append("missing debug group " + ident)
|
||||
|
||||
wanted = [r for r in (opts.get("require_renderers") or "").split(",") if r]
|
||||
if wanted:
|
||||
if not any(m.group(3) is not None and set(wanted) <= set(m.group(3).split(",")) for m in overlays):
|
||||
reasons.append("no overlay line with renderers " + ",".join(wanted))
|
||||
|
||||
for name in opts.get("require_mod") or []:
|
||||
if not any(name.lower() in l.lower() for l in lines):
|
||||
reasons.append("mod " + name + " not in log")
|
||||
|
||||
if opts.get("forbid_dup"):
|
||||
if not dbg_lines:
|
||||
reasons.append("no debug lines dumped")
|
||||
dups = sorted({m.group(3).lstrip().split(":")[0] + ":" for m in dbg_lines
|
||||
if m.group(2) in DUP_IDS and m.group(3).lstrip().startswith(DUP_PREFIXES)})
|
||||
if dups:
|
||||
reasons.append("duplicate vanilla lines in " + "/".join(DUP_IDS) + ": " + ", ".join(dups))
|
||||
|
||||
|
||||
def check(variant_dir, exit_status, new_crashes, opts=None):
|
||||
opts = opts or {}
|
||||
shots_required = opts.get("shots") or SHOTS_REQUIRED
|
||||
variant = os.path.basename(os.path.normpath(variant_dir))
|
||||
reasons = []
|
||||
details = []
|
||||
@@ -71,7 +119,11 @@ def check(variant_dir, exit_status, new_crashes):
|
||||
elif max(frames) == 0:
|
||||
reasons.append("flyby frames is 0")
|
||||
|
||||
bad = [l for l in lines if BAD_LINE.search(l)]
|
||||
if opts.get("modpack"):
|
||||
bad = [l for i, l in enumerate(lines) if BAD_LINE.search(l)
|
||||
and any("angelica" in x.lower() for x in lines[i:i + 4])]
|
||||
else:
|
||||
bad = [l for l in lines if BAD_LINE.search(l)]
|
||||
if bad:
|
||||
reasons.append(str(len(bad)) + " mixin/injection/scene failure line(s)")
|
||||
details.extend([" " + l.strip()[:200] for l in bad[:3]])
|
||||
@@ -94,12 +146,14 @@ def check(variant_dir, exit_status, new_crashes):
|
||||
if CATCH_MARK in l and any(CRASH_MARK in x for x in lines[i:i + 2]):
|
||||
caught = True
|
||||
break
|
||||
if not caught:
|
||||
if not caught and not opts.get("modpack"):
|
||||
reasons.append("no ISBRH catch line for crashtest")
|
||||
|
||||
check_overlay(lines, opts, reasons)
|
||||
|
||||
shots = sorted(glob.glob(os.path.join(variant_dir, "flyby-shot-*.png")))
|
||||
if len(shots) < SHOTS_REQUIRED:
|
||||
reasons.append("only " + str(len(shots)) + " of " + str(SHOTS_REQUIRED) + " screenshots")
|
||||
if len(shots) < shots_required:
|
||||
reasons.append("only " + str(len(shots)) + " of " + str(shots_required) + " screenshots")
|
||||
small = [os.path.basename(p) for p in shots if os.path.getsize(p) < MIN_SHOT_BYTES]
|
||||
if small:
|
||||
reasons.append("small screenshot(s): " + ", ".join(small))
|
||||
@@ -154,12 +208,56 @@ def self_test():
|
||||
("no-isbrh", [l for l in GOOD_LOG if CATCH_MARK not in l], {}, 1),
|
||||
("mixin-error", GOOD_LOG + ["[10:00:05] [main/ERROR]: InvalidInjectionException: bad"], {}, 1),
|
||||
("small-shot", GOOD_LOG, {"shot_size": 100}, 1),
|
||||
("bad-entries", GOOD_LOG + ["[10:00:06] [Client thread/ERROR]: Flyby debugEntries: unknown entry id 'test:nope'"], {}, 1),
|
||||
]
|
||||
for name, log, kw, want in cases:
|
||||
d = make_variant(root, name, log, **kw)
|
||||
got = check(d, 0, 0)
|
||||
if got != want:
|
||||
failures.append(name)
|
||||
no_isbrh = [l for l in GOOD_LOG if CATCH_MARK not in l]
|
||||
ov = [
|
||||
"[10:00:06] [Client thread/INFO]: Flyby debug overlay shot=0 mode=OVERLAY groups=2 renderers=vanilla,angelica",
|
||||
"[10:00:06] [Client thread/INFO]: Flyby debug group shot=0 id=angelica:other_left side=L x=0 y=0 w=1 h=1 facts= lines=2",
|
||||
"[10:00:06] [Client thread/INFO]: Flyby debug line shot=0 id=angelica:other_left text=Allocation rate: 1 MB/s",
|
||||
"[10:00:06] [Client thread/INFO]: Flyby debug line shot=0 id=angelica:other_left text=Second",
|
||||
"[10:00:06] [Client thread/INFO]: Flyby debug line shot=0 id=angelica:other_right text=Free",
|
||||
"[10:00:07] [main/INFO]: Loading mod Botania",
|
||||
]
|
||||
ov_dup = [l for l in ov if "Allocation" not in l]
|
||||
ov_dup += ["[10:00:06] [Client thread/INFO]: Flyby debug line shot=1 id=angelica:other_right text= GPU: foo"]
|
||||
flag_cases = [
|
||||
("modpack-ok", no_isbrh, {"modpack": True}, 0),
|
||||
("modpack-no-flag", no_isbrh, {}, 1),
|
||||
("modpack-foreign-bad", GOOD_LOG + ["[10:00:05] [main/ERROR]: InvalidInjectionException: x", "other", "other", "other", "angelica late"], {"modpack": True}, 0),
|
||||
("modpack-angelica-bad", no_isbrh + ["[10:00:05] [main/ERROR]: Grouped debug screen failed", "x", "angelica y"], {"modpack": True}, 1),
|
||||
("grouped-failed", GOOD_LOG + ["[10:00:05] [main/ERROR]: Grouped debug screen failed"], {}, 1),
|
||||
("setup-failed", GOOD_LOG + ["[10:00:05] [Client thread/ERROR] [Angelica]: Failed to set up the debug screen; it stays disabled for this session"], {}, 1),
|
||||
("entry-failed", GOOD_LOG + ["[10:00:05] [Client thread/ERROR] [Angelica]: Debug screen entry botania:debug failed and is disabled for the rest of this session"], {}, 1),
|
||||
("font-fallback", GOOD_LOG + ["[10:00:05] [Client thread/ERROR] [Angelica]: Grouped debug screen needs the batching font renderer on the HUD font; using the classic layout for the rest of this session"], {}, 1),
|
||||
("shots-ok", GOOD_LOG, {"shots": 3}, 0),
|
||||
("shots-short", GOOD_LOG, {"shots": 9}, 1),
|
||||
("mode-ok", GOOD_LOG + ov, {"expect_mode": "OVERLAY"}, 0),
|
||||
("mode-wrong", GOOD_LOG + ov, {"expect_mode": "FLAT"}, 1),
|
||||
("mode-none", GOOD_LOG, {"expect_mode": "FLAT"}, 1),
|
||||
("group-ok", GOOD_LOG + ov, {"require_group": ["angelica:other_left"]}, 0),
|
||||
("group-missing", GOOD_LOG + ov, {"require_group": ["angelica:nope"]}, 1),
|
||||
("renderers-ok", GOOD_LOG + ov, {"require_renderers": "angelica,vanilla"}, 0),
|
||||
("renderers-missing", GOOD_LOG + ov, {"require_renderers": "angelica,sodium"}, 1),
|
||||
("mod-ok", GOOD_LOG + ov, {"require_mod": ["botania"]}, 0),
|
||||
("mod-missing", GOOD_LOG + ov, {"require_mod": ["thaumcraft"]}, 1),
|
||||
("dup-fail", GOOD_LOG + ov, {"forbid_dup": True}, 1),
|
||||
("dup-ok", GOOD_LOG + ov_dup[:3] + ov_dup[4:5], {"forbid_dup": True}, 0),
|
||||
("dup-spaces-fail", GOOD_LOG + ov_dup, {"forbid_dup": True}, 1),
|
||||
("dup-no-dump", GOOD_LOG, {"forbid_dup": True}, 1),
|
||||
]
|
||||
line = "[10:00:06] [Client thread/INFO]: Flyby debug line shot=1 id=angelica:other_left text= [Botania] x"
|
||||
flag_cases.append(("dup-prefix", GOOD_LOG + ov_dup[:3] + [line], {"forbid_dup": True}, 1))
|
||||
flag_cases.append(("dup-prefix-other-id", GOOD_LOG + ov_dup[:3] + [line.replace("other_left", "other_elsewhere")], {"forbid_dup": True}, 0))
|
||||
for name, log, opts, want in flag_cases:
|
||||
d = make_variant(root, name, log, shots=3 if name == "shots-ok" else 8)
|
||||
if check(d, 0, 0, opts) != want:
|
||||
failures.append(name)
|
||||
d = make_variant(root, "bad-exit", GOOD_LOG)
|
||||
if check(d, "timeout", 1) != 1:
|
||||
failures.append("bad-exit")
|
||||
@@ -175,11 +273,19 @@ def self_test():
|
||||
def main(argv):
|
||||
if len(argv) == 2 and argv[1] == "--self-test":
|
||||
return self_test()
|
||||
if len(argv) != 4:
|
||||
print("usage: flyby-smoke-check.py <variant_dir> <exit_status|timeout> <new_crash_count>", file=sys.stderr)
|
||||
print(" flyby-smoke-check.py --self-test", file=sys.stderr)
|
||||
return 2
|
||||
return check(argv[1], argv[2], int(argv[3]))
|
||||
ap = argparse.ArgumentParser(prog="flyby-smoke-check.py")
|
||||
ap.add_argument("variant_dir")
|
||||
ap.add_argument("exit_status")
|
||||
ap.add_argument("new_crashes", type=int)
|
||||
ap.add_argument("--modpack", action="store_true")
|
||||
ap.add_argument("--shots", type=int)
|
||||
ap.add_argument("--expect-mode")
|
||||
ap.add_argument("--require-group", action="append", metavar="ID")
|
||||
ap.add_argument("--require-renderers", metavar="a,b")
|
||||
ap.add_argument("--require-mod", action="append", metavar="NAME")
|
||||
ap.add_argument("--forbid-dup", action="store_true")
|
||||
a = ap.parse_args(argv[1:])
|
||||
return check(a.variant_dir, a.exit_status, a.new_crashes, vars(a))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+27
-60
@@ -11,11 +11,14 @@
|
||||
# SMOKE_TIMEOUT - seconds before the client is killed (default: 600).
|
||||
# SMOKE_SCENE - scene command file (default: scripts/flyby-scenes/surround-smoke.txt).
|
||||
# SMOKE_PACK - shaderpack name for the pack variant (default: ComplementaryUnbound_r5.7.1 + EuphoriaPatches_1.8.1).
|
||||
# SMOKE_DEBUG_HUD - true forces F3 on; false keeps the saved setting (default: true).
|
||||
# SMOKE_DEBUG_HUD - true forces F3 on, false forces it off (default: true).
|
||||
# SMOKE_PITCH - camera pitch in degrees, positive looks down (default: 15).
|
||||
# SMOKE_WARMUP_TICKS - ticks at the first pose before the pan (default: 100).
|
||||
# SMOKE_ARGS - extra -D flags for the client, word-split (default: none).
|
||||
# SMOKE_CHECK_ARGS - extra flyby-smoke-check.py flags, word-split (default: none).
|
||||
# SMOKE_FULLSCREEN - 0 keeps options.txt's window mode (default: 1, fullscreen for the run).
|
||||
#
|
||||
# Runs from this script's own checkout. Restores run/client/config/shaders.properties and angelica-options.json on exit.
|
||||
# Runs from this script's own checkout, fullscreen unless SMOKE_FULLSCREEN=0 (fullscreen avoids vsync pacing). Restores run/client/config/shaders.properties, angelica-options.json and options.txt on exit.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -27,6 +30,8 @@ fi
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
# shellcheck source=flyby-lib.sh
|
||||
. "$ROOT/scripts/flyby-lib.sh"
|
||||
|
||||
QUICKPLAY_WORLD="${QUICKPLAY_WORLD:-Flat World - No Mods}"
|
||||
FLYBY_ORIGIN="${FLYBY_ORIGIN:-1509.5,-924.5,0}"
|
||||
@@ -35,6 +40,9 @@ SMOKE_PACK="${SMOKE_PACK:-ComplementaryUnbound_r5.7.1 + EuphoriaPatches_1.8.1}"
|
||||
SMOKE_DEBUG_HUD="${SMOKE_DEBUG_HUD:-true}"
|
||||
SMOKE_PITCH="${SMOKE_PITCH:-15}"
|
||||
SMOKE_WARMUP_TICKS="${SMOKE_WARMUP_TICKS:-100}"
|
||||
SMOKE_ARGS="${SMOKE_ARGS:-}"
|
||||
SMOKE_CHECK_ARGS="${SMOKE_CHECK_ARGS:-}"
|
||||
SMOKE_FULLSCREEN="${SMOKE_FULLSCREEN:-1}"
|
||||
|
||||
SCENE="${SMOKE_SCENE:-$ROOT/scripts/flyby-scenes/surround-smoke.txt}"
|
||||
case "$SCENE" in
|
||||
@@ -47,6 +55,7 @@ OUT="$ROOT/tmp/smoke/$NAME"
|
||||
BACKUP="$OUT/shaders.properties.bak"
|
||||
OPTIONS="$RUN/config/angelica-options.json"
|
||||
OPTIONS_BACKUP="$OUT/angelica-options.json.bak"
|
||||
OPTIONS_TXT="$RUN/options.txt"
|
||||
|
||||
for v in "$@"; do
|
||||
case "$v" in
|
||||
@@ -66,48 +75,26 @@ for v in "$@"; do
|
||||
done
|
||||
|
||||
mkdir -p "$OUT"
|
||||
python3 - "$SHADERS" "$BACKUP" <<'PY'
|
||||
import shutil, sys
|
||||
shutil.copyfile(sys.argv[1], sys.argv[2])
|
||||
PY
|
||||
python3 - "$OPTIONS" "$OPTIONS_BACKUP" <<'PY'
|
||||
import shutil, sys
|
||||
shutil.copyfile(sys.argv[1], sys.argv[2])
|
||||
PY
|
||||
flyby_copy "$SHADERS" "$BACKUP"
|
||||
flyby_copy "$OPTIONS" "$OPTIONS_BACKUP"
|
||||
|
||||
restore_options() {
|
||||
python3 - "$OPTIONS_BACKUP" "$OPTIONS" <<'PY' || true
|
||||
import shutil, sys
|
||||
shutil.copyfile(sys.argv[1], sys.argv[2])
|
||||
PY
|
||||
flyby_restore_file "$OPTIONS_BACKUP" "$OPTIONS" || true
|
||||
}
|
||||
|
||||
restore() {
|
||||
python3 - "$BACKUP" "$SHADERS" <<'PY' || true
|
||||
import shutil, sys
|
||||
shutil.copyfile(sys.argv[1], sys.argv[2])
|
||||
PY
|
||||
flyby_restore_file "$BACKUP" "$SHADERS" || true
|
||||
restore_options
|
||||
if [ "$SMOKE_FULLSCREEN" = 1 ]; then flyby_fullscreen_restore "$OPTIONS_TXT" "$OUT/options.txt.bak" || true; fi
|
||||
}
|
||||
if [ "$SMOKE_FULLSCREEN" = 1 ]; then flyby_fullscreen_on "$OPTIONS_TXT" "$OUT/options.txt.bak"; fi
|
||||
trap restore EXIT
|
||||
|
||||
set_shaders() {
|
||||
python3 - "$SHADERS" "$1" "$2" <<'PY'
|
||||
import re, sys
|
||||
path, enable, pack = sys.argv[1:4]
|
||||
text = open(path).read()
|
||||
def put(text, key, value):
|
||||
pat = re.compile(r'^' + key + r'=.*$', re.M)
|
||||
if pat.search(text):
|
||||
return pat.sub(lambda m: key + '=' + value, text)
|
||||
if text and not text.endswith('\n'):
|
||||
text += '\n'
|
||||
return text + key + '=' + value + '\n'
|
||||
text = put(text, 'enableShaders', enable)
|
||||
if pack:
|
||||
text = put(text, 'shaderPack', pack)
|
||||
open(path, 'w').write(text)
|
||||
PY
|
||||
flyby_set_key "$SHADERS" enableShaders "$1" =
|
||||
if [ -n "$2" ]; then
|
||||
flyby_set_key "$SHADERS" shaderPack "$2" =
|
||||
fi
|
||||
}
|
||||
|
||||
set_vao() {
|
||||
@@ -123,20 +110,10 @@ with open(path, "w") as f:
|
||||
PY
|
||||
}
|
||||
|
||||
crash_list() {
|
||||
if [ -d "$RUN/crash-reports" ]; then
|
||||
ls "$RUN/crash-reports" | sort
|
||||
fi
|
||||
}
|
||||
|
||||
FAILED=0
|
||||
for VARIANT in "$@"; do
|
||||
VDIR="$OUT/$VARIANT"
|
||||
python3 - "$VDIR" <<'PY'
|
||||
import shutil, sys
|
||||
shutil.rmtree(sys.argv[1], ignore_errors=True)
|
||||
PY
|
||||
mkdir -p "$VDIR"
|
||||
flyby_fresh_dir "$VDIR"
|
||||
|
||||
restore_options
|
||||
if [ "$VARIANT" = "vao" ]; then
|
||||
@@ -151,7 +128,7 @@ PY
|
||||
|
||||
mkdir -p "$RUN/screenshots"
|
||||
rm -f "$RUN"/screenshots/flyby-shot-*.png
|
||||
crash_list >| "$VDIR/crashes.before"
|
||||
flyby_crash_list "$RUN/crash-reports" >| "$VDIR/crashes.before"
|
||||
|
||||
echo "==> $NAME/$VARIANT: launching (timeout ${SMOKE_TIMEOUT}s)"
|
||||
./gradlew runClient25 --console=plain \
|
||||
@@ -167,6 +144,7 @@ PY
|
||||
-Dangelica.flyby.crashTest=isbrh \
|
||||
-Dangelica.debug.testBlocks=true \
|
||||
-DquickPlaySingleplayer="$QUICKPLAY_WORLD" \
|
||||
$SMOKE_ARGS \
|
||||
>| "$VDIR/console.log" 2>&1 &
|
||||
PID=$!
|
||||
|
||||
@@ -190,21 +168,10 @@ PY
|
||||
STATUS="$RC"
|
||||
fi
|
||||
|
||||
crash_list >| "$VDIR/crashes.after"
|
||||
NEW_CRASHES="$(comm -13 "$VDIR/crashes.before" "$VDIR/crashes.after" | wc -l | tr -d ' ')"
|
||||
NEW_CRASHES="$(flyby_new_crashes "$RUN/crash-reports" "$VDIR/crashes.before" "$VDIR/crashes.after")"
|
||||
flyby_collect "$RUN" "$VDIR" fml-client-latest.log latest.log
|
||||
|
||||
for f in fml-client-latest.log latest.log; do
|
||||
if [ -f "$RUN/logs/$f" ]; then
|
||||
cp -f "$RUN/logs/$f" "$VDIR/$f"
|
||||
fi
|
||||
done
|
||||
for f in "$RUN"/screenshots/flyby-shot-*.png; do
|
||||
if [ -f "$f" ]; then
|
||||
cp -f "$f" "$VDIR/"
|
||||
fi
|
||||
done
|
||||
|
||||
if ! python3 "$ROOT/scripts/flyby-smoke-check.py" "$VDIR" "$STATUS" "$NEW_CRASHES"; then
|
||||
if ! python3 "$ROOT/scripts/flyby-smoke-check.py" "$VDIR" "$STATUS" "$NEW_CRASHES" $SMOKE_CHECK_ARGS; then
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -785,7 +785,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
device.fenceReleaser().drain();
|
||||
if (frameSync != 0L) {
|
||||
frameSyncs.enqueue(frameSync);
|
||||
while (frameSyncs.size() > Device.MAX_FRAMES_IN_FLIGHT - 1) {
|
||||
while (frameSyncs.size() > RenderBackend.MAX_FRAMES_IN_FLIGHT - 1) {
|
||||
final long sync = frameSyncs.dequeueLong();
|
||||
fenceTracker.clientWaitSync(sync, 0, Long.MAX_VALUE);
|
||||
fenceTracker.deleteSync(sync);
|
||||
|
||||
+6
-6
@@ -47,7 +47,6 @@ public final class VertexWriteReplayer {
|
||||
private static final int MAX_DIVISOR = 0xFFFF;
|
||||
private static final int UNCHANGED = -1;
|
||||
private static final int TEXTURE_UNITS = 32;
|
||||
private static final int SAMPLER_OBJECT_OF_DRAW = -1;
|
||||
|
||||
public static final class Replay {
|
||||
final int replayProgram;
|
||||
@@ -60,7 +59,7 @@ public final class VertexWriteReplayer {
|
||||
int[] uniformTargets;
|
||||
int[] samplerUnits;
|
||||
IntSupplier[] samplerSources;
|
||||
int[] samplerObjects;
|
||||
IntSupplier[] samplerObjects;
|
||||
int[] packStorageSlots;
|
||||
int[] packRwStorageSlots;
|
||||
int[] uniformBlockIndices;
|
||||
@@ -143,7 +142,7 @@ public final class VertexWriteReplayer {
|
||||
private void resolveSamplers(ShaderManager.ProgramObject graphics, ShaderManager.ProgramObject replay, List<String> names) {
|
||||
final IntArrayList units = new IntArrayList();
|
||||
final List<IntSupplier> sources = new ArrayList<>();
|
||||
final IntArrayList objects = new IntArrayList();
|
||||
final List<IntSupplier> objects = new ArrayList<>();
|
||||
final IntArrayList ownTextureIndices = new IntArrayList();
|
||||
final List<String> ownTextureNames = new ArrayList<>();
|
||||
final List<String> pipelineNames = Arrays.asList(setup.samplerNames());
|
||||
@@ -164,7 +163,7 @@ public final class VertexWriteReplayer {
|
||||
if (!units.contains(unit)) {
|
||||
units.add(unit);
|
||||
sources.add(null);
|
||||
objects.add(SAMPLER_OBJECT_OF_DRAW);
|
||||
objects.add(null);
|
||||
}
|
||||
}
|
||||
int free = TEXTURE_UNITS - 1;
|
||||
@@ -179,7 +178,7 @@ public final class VertexWriteReplayer {
|
||||
}
|
||||
samplerUnits = units.toIntArray();
|
||||
samplerSources = sources.toArray(new IntSupplier[0]);
|
||||
samplerObjects = objects.toIntArray();
|
||||
samplerObjects = objects.toArray(new IntSupplier[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +305,8 @@ public final class VertexWriteReplayer {
|
||||
final int unit = replay.samplerUnits[i];
|
||||
final IntSupplier source = replay.samplerSources[i];
|
||||
c.samplerTextures[i] = source != null ? source.getAsInt() : st.boundTextures[unit];
|
||||
final int samplerObject = replay.samplerObjects[i] != SAMPLER_OBJECT_OF_DRAW ? replay.samplerObjects[i] : st.boundSamplerObjects[unit];
|
||||
final IntSupplier sampler = replay.samplerObjects[i];
|
||||
final int samplerObject = sampler != null ? sampler.getAsInt() : st.boundSamplerObjects[unit];
|
||||
c.samplerHandles[i] = computeBinder.samplerHandle(samplerObject, c.samplerTextures[i]);
|
||||
}
|
||||
if (c.imageTextures.length != images.length) c.imageTextures = new int[images.length];
|
||||
|
||||
@@ -37,9 +37,7 @@ import static org.lwjgl.system.MemoryUtil.memUTF8;
|
||||
public final class Device {
|
||||
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
|
||||
|
||||
public static final int MAX_FRAMES_IN_FLIGHT = 3;
|
||||
|
||||
private static final int FRAMES_IN_FLIGHT = Math.min(Math.max(SystemProperties.SDL_FRAMES_IN_FLIGHT, 1), MAX_FRAMES_IN_FLIGHT);
|
||||
private static final int FRAMES_IN_FLIGHT = Math.min(Math.max(SystemProperties.SDL_FRAMES_IN_FLIGHT, 1), RenderBackend.MAX_FRAMES_IN_FLIGHT);
|
||||
|
||||
public static int framesInFlight() { return FRAMES_IN_FLIGHT; }
|
||||
|
||||
|
||||
+4
-3
@@ -45,6 +45,7 @@ import static org.lwjgl.system.MemoryStack.*;
|
||||
import static org.lwjgl.system.MemoryUtil.memAlloc;
|
||||
import static org.lwjgl.system.MemoryUtil.memFree;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.glsm.texture.TextureStaging;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
|
||||
@@ -697,7 +698,7 @@ public final class ResourceManager {
|
||||
final GpuBucket b = gpuBufferPool.get(Hashing.packHiLo(usage, bucket));
|
||||
if (b == null || b.handles.isEmpty()) return 0;
|
||||
for (int i = 0, n = b.handles.size(); i < n; i++) {
|
||||
if (gpuPoolCurrentFrame - b.frames.getLong(i) >= Device.MAX_FRAMES_IN_FLIGHT) {
|
||||
if (gpuPoolCurrentFrame - b.frames.getLong(i) >= RenderBackend.MAX_FRAMES_IN_FLIGHT) {
|
||||
final long handle = b.handles.getLong(i);
|
||||
final int last = n - 1;
|
||||
if (i != last) {
|
||||
@@ -1555,7 +1556,7 @@ public final class ResourceManager {
|
||||
for (int i = 0; i < n; i += 2) {
|
||||
final long handle = batchSegmentPending.getLong(i);
|
||||
final long stamp = batchSegmentPending.getLong(i + 1);
|
||||
if (currentFrame - stamp >= Device.MAX_FRAMES_IN_FLIGHT) {
|
||||
if (currentFrame - stamp >= RenderBackend.MAX_FRAMES_IN_FLIGHT) {
|
||||
batchSegmentFree.enqueue(handle);
|
||||
} else {
|
||||
batchSegmentPending.set(w, handle);
|
||||
@@ -2445,7 +2446,7 @@ public final class ResourceManager {
|
||||
}
|
||||
|
||||
private static final int DUMMY_VBO_SIZE = 16 * 16;
|
||||
public static final int ATTRIB_RING_BLOCKS = 2048 * Device.MAX_FRAMES_IN_FLIGHT;
|
||||
public static final int ATTRIB_RING_BLOCKS = 2048 * RenderBackend.MAX_FRAMES_IN_FLIGHT;
|
||||
public static final int ATTRIB_RING_CHUNK_BLOCKS = 64;
|
||||
private static final int ATTRIB_RING_CHUNKS = ATTRIB_RING_BLOCKS / ATTRIB_RING_CHUNK_BLOCKS;
|
||||
private final AtomicLong attribRingCounter = new AtomicLong();
|
||||
|
||||
+2
-12
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.sdlgpu.glsm;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
@@ -12,11 +13,9 @@ import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import sun.misc.Unsafe;
|
||||
|
||||
import static com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig.BLACK;
|
||||
import static com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig.RED;
|
||||
@@ -34,17 +33,8 @@ class GlsmSdlAttachmentClearTest {
|
||||
return GlsmSdlHeadlessRig.frameManager().frame();
|
||||
}
|
||||
|
||||
// Reflect.setStaticFinal rejects primitive fields.
|
||||
private static void setDisableInPassClear(boolean value) {
|
||||
try {
|
||||
final Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
|
||||
theUnsafe.setAccessible(true);
|
||||
final Unsafe unsafe = (Unsafe) theUnsafe.get(null);
|
||||
final Field field = SystemProperties.class.getDeclaredField("SDL_DISABLE_IN_PASS_CLEAR");
|
||||
unsafe.putBoolean(unsafe.staticFieldBase(field), unsafe.staticFieldOffset(field), value);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
Reflect.setStaticFinal(SystemProperties.class, "SDL_DISABLE_IN_PASS_CLEAR", boolean.class, value);
|
||||
}
|
||||
|
||||
/** Clears colour to black and depth to 0.9, then draws a quad so a render pass is open. */
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
import net.minecraft.client.entity.EntityClientPlayerMP;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
|
||||
public interface DebugContext {
|
||||
|
||||
World world();
|
||||
|
||||
EntityClientPlayerMP player();
|
||||
|
||||
int blockX();
|
||||
|
||||
int blockY();
|
||||
|
||||
int blockZ();
|
||||
|
||||
Chunk chunk();
|
||||
|
||||
long nanoTime();
|
||||
|
||||
boolean isSampleDue();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public enum DebugEntryCategory {
|
||||
|
||||
SCREEN_TEXT("angelica.debug.options.category.text"),
|
||||
RENDERER("angelica.debug.options.category.renderer");
|
||||
|
||||
public final String translationKey;
|
||||
|
||||
DebugEntryCategory(String translationKey) {
|
||||
this.translationKey = translationKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public interface DebugEntryHandle {
|
||||
|
||||
String id();
|
||||
|
||||
boolean isEnabled();
|
||||
|
||||
boolean isFailed();
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public final class DebugEntrySpec {
|
||||
|
||||
public interface DebugEntryFactory {
|
||||
|
||||
DebugScreenEntry create(DebugEntryHandle entry);
|
||||
}
|
||||
|
||||
public interface DebugToggleListener {
|
||||
|
||||
void onEnabledChanged(DebugEntryHandle entry, boolean enabled);
|
||||
}
|
||||
|
||||
private final String id;
|
||||
private final DebugEntryCategory category;
|
||||
private String name;
|
||||
private DebugScreenEntryStatus defaultStatus;
|
||||
private DebugScreenEntryStatus performanceStatus = DebugScreenEntryStatus.NEVER;
|
||||
private DebugEntryFactory textFactory;
|
||||
private DebugToggleListener toggleListener;
|
||||
private boolean supportsAlways = true;
|
||||
|
||||
public DebugEntrySpec(String id, DebugEntryCategory category) {
|
||||
this.id = id;
|
||||
this.category = category;
|
||||
this.defaultStatus = category == DebugEntryCategory.SCREEN_TEXT ? DebugScreenEntryStatus.IN_OVERLAY : DebugScreenEntryStatus.NEVER;
|
||||
}
|
||||
|
||||
public DebugEntrySpec name(String keyOrLiteral) {
|
||||
this.name = keyOrLiteral;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugEntrySpec defaults(DebugScreenEntryStatus defaultStatus, DebugScreenEntryStatus performanceStatus) {
|
||||
this.defaultStatus = defaultStatus;
|
||||
this.performanceStatus = performanceStatus;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugEntrySpec text(DebugEntryFactory factory) {
|
||||
this.textFactory = factory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugEntrySpec toggle(DebugToggleListener listener) {
|
||||
this.toggleListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugEntrySpec supportsAlways(boolean supportsAlways) {
|
||||
this.supportsAlways = supportsAlways;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public DebugEntryCategory getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public DebugScreenEntryStatus getDefaultStatus() {
|
||||
return defaultStatus;
|
||||
}
|
||||
|
||||
public DebugScreenEntryStatus getPerformanceStatus() {
|
||||
return performanceStatus;
|
||||
}
|
||||
|
||||
public DebugEntryFactory getTextFactory() {
|
||||
return textFactory;
|
||||
}
|
||||
|
||||
public DebugToggleListener getToggleListener() {
|
||||
return toggleListener;
|
||||
}
|
||||
|
||||
public boolean getSupportsAlways() {
|
||||
return supportsAlways;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public interface DebugFact {
|
||||
|
||||
DebugFact text(CharSequence text);
|
||||
|
||||
DebugFact text(char c);
|
||||
|
||||
DebugFact value(CharSequence value);
|
||||
|
||||
DebugFact value(int value);
|
||||
|
||||
DebugFact value(long value);
|
||||
|
||||
DebugFact value(double value, int decimals);
|
||||
|
||||
DebugFact padded(long value, int width);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class DebugFactLines {
|
||||
|
||||
private static final int MAX_LABEL = 24;
|
||||
private static final String COLON = ": ";
|
||||
private static final String DASH = " - ";
|
||||
|
||||
private String[] sources = new String[8];
|
||||
private String[] names = new String[8];
|
||||
private String[] values = new String[8];
|
||||
private int cursor;
|
||||
private int size;
|
||||
private boolean open;
|
||||
|
||||
public void begin() {
|
||||
cursor = 0;
|
||||
open = true;
|
||||
}
|
||||
|
||||
public void end() {
|
||||
open = false;
|
||||
if (cursor < size) {
|
||||
Arrays.fill(sources, cursor, size, null);
|
||||
Arrays.fill(names, cursor, size, null);
|
||||
Arrays.fill(values, cursor, size, null);
|
||||
}
|
||||
size = cursor;
|
||||
}
|
||||
|
||||
public void fact(String name, String value) {
|
||||
requireOpen();
|
||||
if (value == null) throw new IllegalArgumentException("Debug fact value is null");
|
||||
reserve();
|
||||
sources[cursor] = null;
|
||||
names[cursor] = name;
|
||||
values[cursor] = value;
|
||||
cursor++;
|
||||
}
|
||||
|
||||
public void line(String line) {
|
||||
fact(null, line);
|
||||
}
|
||||
|
||||
public void split(String line) {
|
||||
requireOpen();
|
||||
if (line == null) throw new IllegalArgumentException("Debug fact line is null");
|
||||
reserve();
|
||||
if (line.equals(sources[cursor])) {
|
||||
cursor++;
|
||||
return;
|
||||
}
|
||||
sources[cursor] = line;
|
||||
parse(line);
|
||||
cursor++;
|
||||
}
|
||||
|
||||
public void emit(DebugScreenDisplayer displayer, DebugGroupHandle group) {
|
||||
if (open) throw new IllegalStateException("DebugFactLines.emit() called before end()");
|
||||
for (int i = 0; i < size; i++) {
|
||||
final String value = values[i];
|
||||
final String name = names[i];
|
||||
if (name == null) {
|
||||
displayer.line(group).text(value);
|
||||
} else {
|
||||
displayer.fact(group, name).value(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parse(String line) {
|
||||
int at = labelEnd(line, COLON);
|
||||
if (at >= 0) {
|
||||
names[cursor] = name(line, at);
|
||||
values[cursor] = line.substring(at + COLON.length());
|
||||
return;
|
||||
}
|
||||
at = labelEnd(line, DASH);
|
||||
if (at >= 0) {
|
||||
names[cursor] = name(line, at);
|
||||
values[cursor] = line.substring(at + DASH.length());
|
||||
return;
|
||||
}
|
||||
names[cursor] = null;
|
||||
values[cursor] = line;
|
||||
}
|
||||
|
||||
private String name(String line, int end) {
|
||||
final String previous = names[cursor];
|
||||
return previous != null && previous.length() == end && line.startsWith(previous) ? previous : line.substring(0, end);
|
||||
}
|
||||
|
||||
private void requireOpen() {
|
||||
if (!open) throw new IllegalStateException("DebugFactLines is not open: call begin() first");
|
||||
}
|
||||
|
||||
private void reserve() {
|
||||
if (cursor == sources.length) {
|
||||
final int capacity = cursor * 2;
|
||||
sources = Arrays.copyOf(sources, capacity);
|
||||
names = Arrays.copyOf(names, capacity);
|
||||
values = Arrays.copyOf(values, capacity);
|
||||
}
|
||||
}
|
||||
|
||||
static int labelEnd(String line, String separator) {
|
||||
final int at = line.indexOf(separator);
|
||||
if (at <= 0 || at > MAX_LABEL) return -1;
|
||||
for (int i = 0; i < at; i++) {
|
||||
final char c = line.charAt(i);
|
||||
if (c == '=' || c == '(') return -1;
|
||||
}
|
||||
return at;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public interface DebugGroupHandle {
|
||||
|
||||
int LEFT = 0;
|
||||
int RIGHT = 1;
|
||||
|
||||
String id();
|
||||
|
||||
int side();
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public final class DebugGroupSpec {
|
||||
|
||||
public static final int DEFAULT_ORDER = 500;
|
||||
|
||||
private final String id;
|
||||
private String titleKey;
|
||||
private int accent = -1;
|
||||
private int side = DebugGroupHandle.LEFT;
|
||||
private int order = DEFAULT_ORDER;
|
||||
|
||||
public DebugGroupSpec(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public DebugGroupSpec title(String keyOrLiteral) {
|
||||
this.titleKey = keyOrLiteral;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugGroupSpec accent(int rgb) {
|
||||
this.accent = rgb & 0xFFFFFF;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugGroupSpec side(int side) {
|
||||
this.side = side;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugGroupSpec order(int order) {
|
||||
this.order = order;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getTitleKey() {
|
||||
return titleKey;
|
||||
}
|
||||
|
||||
public int getAccent() {
|
||||
return accent;
|
||||
}
|
||||
|
||||
public int getSide() {
|
||||
return side;
|
||||
}
|
||||
|
||||
public int getOrder() {
|
||||
return order;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugRegistry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreen;
|
||||
|
||||
public final class DebugScreenApi {
|
||||
|
||||
private DebugScreenApi() {}
|
||||
|
||||
public static DebugGroupHandle registerGroup(DebugGroupSpec spec) {
|
||||
DebugScreen.bootstrap();
|
||||
return DebugRegistry.MAIN.registerGroup(spec);
|
||||
}
|
||||
|
||||
public static DebugEntryHandle registerEntry(DebugEntrySpec spec) {
|
||||
DebugScreen.bootstrap();
|
||||
return DebugRegistry.MAIN.registerEntry(spec);
|
||||
}
|
||||
|
||||
public static DebugGroupHandle group(String id) {
|
||||
DebugScreen.bootstrap();
|
||||
return DebugRegistry.MAIN.groupById(id);
|
||||
}
|
||||
|
||||
public static DebugEntryHandle entry(String id) {
|
||||
DebugScreen.bootstrap();
|
||||
return DebugRegistry.MAIN.entryById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public interface DebugScreenDisplayer {
|
||||
|
||||
DebugFact line(DebugGroupHandle group);
|
||||
|
||||
DebugFact fact(DebugGroupHandle group, String name);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public interface DebugScreenEntry {
|
||||
|
||||
void display(DebugScreenDisplayer displayer, DebugContext context);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public enum DebugScreenEntryStatus {
|
||||
|
||||
ALWAYS_ON("alwaysOn"),
|
||||
IN_OVERLAY("inOverlay"),
|
||||
NEVER("never");
|
||||
|
||||
private static final DebugScreenEntryStatus[] VALUES = values();
|
||||
|
||||
public final String serializedName;
|
||||
|
||||
DebugScreenEntryStatus(String serializedName) {
|
||||
this.serializedName = serializedName;
|
||||
}
|
||||
|
||||
public static DebugScreenEntryStatus bySerializedName(String name) {
|
||||
for (final DebugScreenEntryStatus status : VALUES) {
|
||||
if (status.serializedName.equals(name)) return status;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@API(owner = "angelica", provides = "angelica|debug", apiVersion = "1")
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
import cpw.mods.fml.common.API;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -48,10 +48,6 @@ public final class FontEffectRegistry {
|
||||
return anyRegistered && lowerCode >= 'a' && lowerCode <= 'z' && EFFECTS[lowerCode - 'a'] != null;
|
||||
}
|
||||
|
||||
static long bit(char lowerCode) {
|
||||
return 1L << (lowerCode - 'a');
|
||||
}
|
||||
|
||||
static CustomGlyphEffect get(int bitIndex) {
|
||||
return EFFECTS[bitIndex];
|
||||
}
|
||||
|
||||
@@ -31,15 +31,19 @@ public final class FontProviderBitmap implements FontProvider {
|
||||
|
||||
private static final ResourceLocation DEFINITION = new ResourceLocation("angelica", "font/modern.json");
|
||||
private static final int DEFAULT_HEIGHT = 8;
|
||||
private static final FontProviderBitmap[] NONE = new FontProviderBitmap[0];
|
||||
static final FontProviderBitmap[] NONE = new FontProviderBitmap[0];
|
||||
|
||||
private static volatile FontProviderBitmap[] loaded;
|
||||
|
||||
public static void registerReloadListener() {
|
||||
((IReloadableResourceManager) Minecraft.getMinecraft().getResourceManager()).registerReloadListener(manager -> loaded = null);
|
||||
((IReloadableResourceManager) Minecraft.getMinecraft().getResourceManager()).registerReloadListener(manager -> {
|
||||
loaded = null;
|
||||
FontProviderCache.invalidate();
|
||||
});
|
||||
}
|
||||
|
||||
private final ResourceLocation sheet;
|
||||
private int texture;
|
||||
private final int columns;
|
||||
private final float uSize;
|
||||
private final float vSize;
|
||||
@@ -60,7 +64,7 @@ public final class FontProviderBitmap implements FontProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static FontProviderBitmap[] providers() {
|
||||
static FontProviderBitmap[] providers() {
|
||||
FontProviderBitmap[] local = loaded;
|
||||
if (local == null) {
|
||||
final IResourceManager resources = Minecraft.getMinecraft().getResourceManager();
|
||||
@@ -229,7 +233,12 @@ public final class FontProviderBitmap implements FontProvider {
|
||||
|
||||
@Override
|
||||
public int getTexture(char chr) {
|
||||
return FontStrategist.getIntFromResourceLocation(this.sheet);
|
||||
int texture = this.texture;
|
||||
if (texture == 0) {
|
||||
texture = FontStrategist.getIntFromResourceLocation(this.sheet);
|
||||
this.texture = texture;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
|
||||
final class FontProviderCache {
|
||||
|
||||
private static final byte UNRESOLVED = 0;
|
||||
private static final byte MC = 1;
|
||||
private static final byte UNICODE = 2;
|
||||
private static final byte CUSTOM_PRIMARY = 3;
|
||||
private static final byte CUSTOM_FALLBACK = 4;
|
||||
private static final byte BITMAP = 5;
|
||||
private static final int MAX_BITMAPS = Byte.MAX_VALUE - BITMAP;
|
||||
private static final int VARIANT_COUNT = 8;
|
||||
|
||||
private static FontProviderCache[] variants = new FontProviderCache[VARIANT_COUNT];
|
||||
private static int generation;
|
||||
|
||||
private final boolean custom;
|
||||
private final boolean unicode;
|
||||
private final FontProviderBitmap[] bitmaps;
|
||||
private final byte[][] pages = new byte[256][];
|
||||
|
||||
private FontProviderCache(boolean custom, boolean unicode, boolean modern) {
|
||||
this.custom = custom;
|
||||
this.unicode = unicode;
|
||||
this.bitmaps = modern && !unicode ? FontProviderBitmap.providers() : FontProviderBitmap.NONE;
|
||||
}
|
||||
|
||||
static FontProvider lookup(char chr, boolean custom, boolean unicode) {
|
||||
final int variant = (custom ? 4 : 0) | (unicode ? 2 : 0) | (FontConfig.enableModernFont ? 1 : 0);
|
||||
final FontProviderCache[] tables = variants;
|
||||
FontProviderCache table = tables[variant];
|
||||
if (table == null) {
|
||||
table = new FontProviderCache(custom, unicode, (variant & 1) != 0);
|
||||
tables[variant] = table;
|
||||
}
|
||||
return table.bitmaps.length > MAX_BITMAPS ? null : table.get(chr);
|
||||
}
|
||||
|
||||
static void invalidate() {
|
||||
generation++;
|
||||
variants = new FontProviderCache[VARIANT_COUNT];
|
||||
}
|
||||
|
||||
static int generation() {
|
||||
return generation;
|
||||
}
|
||||
|
||||
private FontProvider get(char chr) {
|
||||
final int hi = chr >>> 8;
|
||||
byte[] page = pages[hi];
|
||||
if (page == null) {
|
||||
page = new byte[256];
|
||||
pages[hi] = page;
|
||||
}
|
||||
final int lo = chr & 0xFF;
|
||||
byte id = page[lo];
|
||||
if (id == UNRESOLVED) {
|
||||
id = resolve(chr);
|
||||
page[lo] = id;
|
||||
}
|
||||
return switch (id) {
|
||||
case MC -> FontProviderMC.get(false);
|
||||
case UNICODE -> FontProviderUnicode.get();
|
||||
case CUSTOM_PRIMARY -> FontProviderCustom.getPrimary();
|
||||
case CUSTOM_FALLBACK -> FontProviderCustom.getFallback();
|
||||
default -> bitmaps[id - BITMAP];
|
||||
};
|
||||
}
|
||||
|
||||
private byte resolve(char chr) {
|
||||
if (custom) {
|
||||
if (FontProviderCustom.getPrimary().isGlyphAvailable(chr)) {
|
||||
return CUSTOM_PRIMARY;
|
||||
}
|
||||
if (FontProviderCustom.getFallback().isGlyphAvailable(chr)) {
|
||||
return CUSTOM_FALLBACK;
|
||||
}
|
||||
final int bitmap = bitmapIndex(chr);
|
||||
return bitmap >= 0 ? (byte) (BITMAP + bitmap) : UNICODE;
|
||||
}
|
||||
final int bitmap = bitmapIndex(chr);
|
||||
if (bitmap >= 0) {
|
||||
return (byte) (BITMAP + bitmap);
|
||||
}
|
||||
return !unicode && FontProviderMC.get(false).isGlyphAvailable(chr) ? MC : UNICODE;
|
||||
}
|
||||
|
||||
private int bitmapIndex(char chr) {
|
||||
for (int i = 0; i < bitmaps.length; i++) {
|
||||
if (bitmaps[i].isGlyphAvailable(chr)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ public final class FontProviderCustom implements FontProvider {
|
||||
this.atlasTextures[i] = 0;
|
||||
}
|
||||
}
|
||||
FontProviderCache.invalidate();
|
||||
}
|
||||
|
||||
public void reloadFont(int fontID) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
import it.unimi.dsi.fastutil.chars.Char2ShortOpenHashMap;
|
||||
import jss.util.RandomXoshiro256StarStar;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class FontProviderMC implements FontProvider {
|
||||
|
||||
/**
|
||||
@@ -22,30 +24,38 @@ public final class FontProviderMC implements FontProvider {
|
||||
return (isSGA ? InstLoader.instanceSGA : InstLoader.instance);
|
||||
}
|
||||
|
||||
private static final Char2ShortOpenHashMap MCFONT_ASCII_MAP = new Char2ShortOpenHashMap();
|
||||
private static final short[] MCFONT_SLOTS;
|
||||
ResourceLocation locationFontTexture = null;
|
||||
private record TextureSlot(ResourceLocation location, int id) {}
|
||||
|
||||
private TextureSlot textureSlot = new TextureSlot(null, 0);
|
||||
private final RandomXoshiro256StarStar fontRandom = new RandomXoshiro256StarStar();
|
||||
public int[] charWidth;
|
||||
|
||||
static {
|
||||
MCFONT_ASCII_MAP.defaultReturnValue((short) -1);
|
||||
int maxChar = 0;
|
||||
for (int i = 0; i < MCFONT_CHARS.length(); i++) {
|
||||
maxChar = Math.max(maxChar, MCFONT_CHARS.charAt(i));
|
||||
}
|
||||
MCFONT_SLOTS = new short[maxChar + 1];
|
||||
Arrays.fill(MCFONT_SLOTS, (short) -1);
|
||||
for (short i = 0; i < MCFONT_CHARS.length(); i++) {
|
||||
MCFONT_ASCII_MAP.put(MCFONT_CHARS.charAt(i), i);
|
||||
MCFONT_SLOTS[MCFONT_CHARS.charAt(i)] = i;
|
||||
}
|
||||
}
|
||||
|
||||
public int lookupMcFontPosition(char ch) {
|
||||
return MCFONT_ASCII_MAP.get(ch);
|
||||
return ch < MCFONT_SLOTS.length ? MCFONT_SLOTS[ch] : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGlyphAvailable(char chr) {
|
||||
return MCFONT_ASCII_MAP.containsKey(chr);
|
||||
return lookupMcFontPosition(chr) != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* A random glyph of the same width, for {@code §k}. Only slots the reverse lookup
|
||||
* agrees with are eligible: {@link #MCFONT_CHARS} repeats {@code U+0000} and the map
|
||||
* agrees with are eligible: {@link #MCFONT_CHARS} repeats {@code U+0000} and the table
|
||||
* keeps the last, so other slots would measure from a different one. Bounded, since
|
||||
* a resource pack may ship widths where nothing matches.
|
||||
*/
|
||||
@@ -61,7 +71,7 @@ public final class FontProviderMC implements FontProvider {
|
||||
continue;
|
||||
}
|
||||
final char replacement = MCFONT_CHARS.charAt(candidate);
|
||||
if (MCFONT_ASCII_MAP.get(replacement) == candidate) {
|
||||
if (lookupMcFontPosition(replacement) == candidate) {
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
@@ -110,7 +120,13 @@ public final class FontProviderMC implements FontProvider {
|
||||
|
||||
@Override
|
||||
public int getTexture(char chr) {
|
||||
return FontStrategist.getIntFromResourceLocation(locationFontTexture);
|
||||
final ResourceLocation location = locationFontTexture;
|
||||
TextureSlot slot = textureSlot;
|
||||
if (!Objects.equals(slot.location(), location)) {
|
||||
slot = new TextureSlot(location, FontStrategist.getIntFromResourceLocation(location));
|
||||
textureSlot = slot;
|
||||
}
|
||||
return slot.id();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -34,6 +34,7 @@ public final class FontProviderUnicode implements FontProvider, IResourceManager
|
||||
public static FontProviderUnicode get() { return FontProviderUnicode.InstLoader.instance; }
|
||||
|
||||
private static final ResourceLocation[] unicodePageLocations = new ResourceLocation[256];
|
||||
private static final int[] unicodePageTextures = new int[256];
|
||||
private final byte[] glyphWidth = new byte[65536];
|
||||
private final RandomXoshiro256StarStar fontRandom = new RandomXoshiro256StarStar();
|
||||
|
||||
@@ -129,8 +130,12 @@ public final class FontProviderUnicode implements FontProvider, IResourceManager
|
||||
@Override
|
||||
public int getTexture(char chr) {
|
||||
final int uniPage = chr / 256;
|
||||
ResourceLocation rl = getUnicodePageLocation(uniPage);
|
||||
return FontStrategist.getIntFromResourceLocation(rl);
|
||||
int texture = unicodePageTextures[uniPage];
|
||||
if (texture == 0) {
|
||||
texture = FontStrategist.getIntFromResourceLocation(getUnicodePageLocation(uniPage));
|
||||
unicodePageTextures[uniPage] = texture;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -161,6 +161,14 @@ public class FontStrategist {
|
||||
if (me.bookMode) {
|
||||
return FontProviderUnicode.get();
|
||||
}
|
||||
if (!me.isSplash && GLStateManager.isMainThread()) {
|
||||
final FontProvider cached = FontProviderCache.lookup(chr, customFontEnabled, forceUnicode);
|
||||
if (cached != null) { return cached; }
|
||||
}
|
||||
return searchFontProvider(me, chr, customFontEnabled, forceUnicode);
|
||||
}
|
||||
|
||||
static FontProvider searchFontProvider(BatchingFontRenderer me, char chr, boolean customFontEnabled, boolean forceUnicode) {
|
||||
if (customFontEnabled && !me.isSplash) {
|
||||
FontProvider fp;
|
||||
fp = FontProviderCustom.getPrimary();
|
||||
@@ -235,23 +243,30 @@ public class FontStrategist {
|
||||
static final int INVALID_TEXTURE = Integer.MIN_VALUE;
|
||||
private static int remap(int n) { return -1 - n; } // involution
|
||||
|
||||
private static final Object RESOURCE_LOCK = new Object();
|
||||
private static final List<ResourceLocation> intToResourceLocation = new ArrayList<>();
|
||||
private static final Map<ResourceLocation, Integer> resourceLocationToInt = new HashMap<>();
|
||||
|
||||
static int getIntFromResourceLocation(ResourceLocation rl) {
|
||||
Integer i = resourceLocationToInt.get(rl);
|
||||
if (i != null) { return i; }
|
||||
int idx = remap(intToResourceLocation.size());
|
||||
resourceLocationToInt.put(rl, idx);
|
||||
intToResourceLocation.add(rl);
|
||||
return idx;
|
||||
synchronized (RESOURCE_LOCK) {
|
||||
Integer i = resourceLocationToInt.get(rl);
|
||||
if (i != null) { return i; }
|
||||
int idx = remap(intToResourceLocation.size());
|
||||
resourceLocationToInt.put(rl, idx);
|
||||
intToResourceLocation.add(rl);
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
|
||||
static void bindIntTexture(BatchingFontRenderer owner, int texture) {
|
||||
if (texture > 0) {
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
|
||||
} else if (texture < 0) {
|
||||
((FontRendererAccessor) owner.underlying).angelica$bindTexture(intToResourceLocation.get(remap(texture)));
|
||||
final ResourceLocation location;
|
||||
synchronized (RESOURCE_LOCK) {
|
||||
location = intToResourceLocation.get(remap(texture));
|
||||
}
|
||||
((FontRendererAccessor) owner.underlying).angelica$bindTexture(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.util.font.FontRendering;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.GRADIENT_PAYLOAD;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_LENGTH;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_PAYLOAD;
|
||||
|
||||
public final class FormatCodes {
|
||||
|
||||
public static final int NONE = 0;
|
||||
public static final int HEX = 1;
|
||||
public static final int COLOR = 2;
|
||||
public static final int OBFUSCATED = 3;
|
||||
public static final int BOLD = 4;
|
||||
public static final int STRIKETHROUGH = 5;
|
||||
public static final int UNDERLINE = 6;
|
||||
public static final int ITALIC = 7;
|
||||
public static final int RAINBOW = 8;
|
||||
public static final int WAVE = 9;
|
||||
public static final int DINNERBONE = 10;
|
||||
public static final int SHADOW_COLOR = 11;
|
||||
public static final int SHADOW_TOGGLE = 12;
|
||||
public static final int GRADIENT = 13;
|
||||
public static final int CUSTOM = 14;
|
||||
public static final int RESET = 15;
|
||||
|
||||
private static final int EXTRA_SHIFT = 8;
|
||||
private static final int PAYLOAD_SHIFT = 16;
|
||||
private static final long RGB_MASK = 0xFFFFFFL;
|
||||
|
||||
private FormatCodes() {}
|
||||
|
||||
public static long parse(CharSequence s, int codeIndex, int end) {
|
||||
final char code = Character.toLowerCase(s.charAt(codeIndex));
|
||||
return switch (code) {
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> pack(COLOR, 0, code - '0');
|
||||
case 'a', 'b', 'c', 'd', 'e', 'f' -> pack(COLOR, 0, code - 'a' + 10);
|
||||
case 'k' -> OBFUSCATED;
|
||||
case 'l' -> BOLD;
|
||||
case 'm' -> STRIKETHROUGH;
|
||||
case 'n' -> UNDERLINE;
|
||||
case 'o' -> ITALIC;
|
||||
case 'r' -> RESET;
|
||||
case 'q' -> AngelicaConfig.enableRainbow ? RAINBOW : NONE;
|
||||
case 'z' -> AngelicaConfig.enableWaveText ? WAVE : NONE;
|
||||
case 'v' -> AngelicaConfig.enableDinnerboneText ? DINNERBONE : NONE;
|
||||
case 'x' -> {
|
||||
if (!AngelicaConfig.enableRGBColors || codeIndex + SECTION_X_PAYLOAD >= end) yield NONE;
|
||||
final int rgb = ColorCodeUtils.parseHexPairs(s, codeIndex + 1, 6);
|
||||
yield rgb == -1 ? NONE : pack(HEX, SECTION_X_PAYLOAD, rgb);
|
||||
}
|
||||
case 'u' -> {
|
||||
if (!AngelicaConfig.enableDropShadow) yield NONE;
|
||||
final int rgb = codeIndex + SECTION_X_LENGTH < end ? ColorCodeUtils.parseSectionXAt(s, codeIndex + 1) : -1;
|
||||
yield rgb == -1 ? SHADOW_TOGGLE : pack(SHADOW_COLOR, SECTION_X_LENGTH, rgb);
|
||||
}
|
||||
case 'g' -> {
|
||||
if (!AngelicaConfig.enableGradients || codeIndex + GRADIENT_PAYLOAD >= end) yield NONE;
|
||||
final int from = ColorCodeUtils.parseSectionXAt(s, codeIndex + 1);
|
||||
final int to = ColorCodeUtils.parseSectionXAt(s, codeIndex + 1 + SECTION_X_LENGTH);
|
||||
yield from == -1 || to == -1 ? NONE : pack(GRADIENT, GRADIENT_PAYLOAD, ((long) from << 24) | to);
|
||||
}
|
||||
default -> FontEffectRegistry.isRegistered(code) ? pack(CUSTOM, 0, code - 'a') : NONE;
|
||||
};
|
||||
}
|
||||
|
||||
private static long pack(int kind, int extraChars, long payload) {
|
||||
return kind | ((long) extraChars << EXTRA_SHIFT) | (payload << PAYLOAD_SHIFT);
|
||||
}
|
||||
|
||||
public static int kind(long code) {
|
||||
return (int) (code & 0xFF);
|
||||
}
|
||||
|
||||
public static int extraChars(long code) {
|
||||
return (int) ((code >>> EXTRA_SHIFT) & 0xFF);
|
||||
}
|
||||
|
||||
public static int rgb(long code) {
|
||||
return (int) ((code >>> PAYLOAD_SHIFT) & RGB_MASK);
|
||||
}
|
||||
|
||||
public static int colorIndex(long code) {
|
||||
return (int) ((code >>> PAYLOAD_SHIFT) & 0xF);
|
||||
}
|
||||
|
||||
public static int gradientFrom(long code) {
|
||||
return (int) ((code >>> (PAYLOAD_SHIFT + 24)) & RGB_MASK);
|
||||
}
|
||||
|
||||
public static int gradientTo(long code) {
|
||||
return rgb(code);
|
||||
}
|
||||
|
||||
public static long customBit(long code) {
|
||||
return 1L << ((code >>> PAYLOAD_SHIFT) & 0x1F);
|
||||
}
|
||||
|
||||
public static boolean endsBold(long code) {
|
||||
return switch (kind(code)) {
|
||||
case COLOR, RESET -> true;
|
||||
case HEX -> FontRendering.hexColorResetsStyles();
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
|
||||
record GlyphScale(int scaleBits, int aspectBits, float x, float y) {
|
||||
|
||||
private static GlyphScale current = of(FontConfig.glyphScale, FontConfig.glyphAspect);
|
||||
|
||||
private static GlyphScale of(float scale, float aspect) {
|
||||
final double pow = Math.pow(2, aspect);
|
||||
return new GlyphScale(Float.floatToRawIntBits(scale), Float.floatToRawIntBits(aspect), (float) (scale * pow), (float) (scale / pow));
|
||||
}
|
||||
|
||||
static GlyphScale current() {
|
||||
final float scale = FontConfig.glyphScale;
|
||||
final float aspect = FontConfig.glyphAspect;
|
||||
GlyphScale holder = current;
|
||||
if (holder.scaleBits != Float.floatToRawIntBits(scale) || holder.aspectBits != Float.floatToRawIntBits(aspect)) {
|
||||
holder = of(scale, aspect);
|
||||
current = holder;
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.util.font.FontRendering;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
|
||||
|
||||
public record GlyphSettings(int generation, int fontAAMode, int fontAAStrength, int shadowCopies, int boldCopies, int fontShadowOffset, int fontShadowOffsetUC,
|
||||
int glyphScale, int glyphAspect, int glyphSpacing, int whitespaceScale, int customFontScale, int waveAmplitude, int flags) {
|
||||
|
||||
private static final int CUSTOM_FONT = 1;
|
||||
private static final int MODERN_FONT = 1 << 1;
|
||||
private static final int GLYPH_REPLACEMENTS = 1 << 2;
|
||||
private static final int RGB_COLORS = 1 << 3;
|
||||
private static final int GRADIENTS = 1 << 4;
|
||||
private static final int RAINBOW = 1 << 5;
|
||||
private static final int WAVE = 1 << 6;
|
||||
private static final int DINNERBONE = 1 << 7;
|
||||
private static final int DROP_SHADOW = 1 << 8;
|
||||
private static final int HEX_RESETS_STYLES = 1 << 9;
|
||||
private static final int CACHE_OVERRIDE = 1 << 10;
|
||||
private static final int METRIC_FLAGS = CUSTOM_FONT | MODERN_FONT | GLYPH_REPLACEMENTS | RGB_COLORS | GRADIENTS | DROP_SHADOW | HEX_RESETS_STYLES;
|
||||
|
||||
// One slot per renderingCacheOverride value: the HUD-cache redraw flips it every refresh, and a single slot would allocate on each flip.
|
||||
private static final GlyphSettings[] CURRENT = new GlyphSettings[2];
|
||||
|
||||
public static GlyphSettings current() {
|
||||
final boolean cacheOverride = HUDCaching.renderingCacheOverride;
|
||||
final int generation = FontProviderCache.generation();
|
||||
final int fontAAMode = FontConfig.fontAAMode;
|
||||
final int fontAAStrength = FontConfig.fontAAStrength;
|
||||
final int shadowCopies = FontConfig.shadowCopies;
|
||||
final int boldCopies = FontConfig.boldCopies;
|
||||
final int fontShadowOffset = Float.floatToIntBits(FontConfig.fontShadowOffset);
|
||||
final int fontShadowOffsetUC = Float.floatToIntBits(FontConfig.fontShadowOffsetUC);
|
||||
final int glyphScale = Float.floatToIntBits(FontConfig.glyphScale);
|
||||
final int glyphAspect = Float.floatToIntBits(FontConfig.glyphAspect);
|
||||
final int glyphSpacing = Float.floatToIntBits(FontConfig.glyphSpacing);
|
||||
final int whitespaceScale = Float.floatToIntBits(FontConfig.whitespaceScale);
|
||||
final int customFontScale = Float.floatToIntBits(FontConfig.customFontScale);
|
||||
final int waveAmplitude = Float.floatToIntBits(AngelicaConfig.waveAmplitude);
|
||||
final int flags = (FontConfig.enableCustomFont ? CUSTOM_FONT : 0)
|
||||
| (FontConfig.enableModernFont ? MODERN_FONT : 0)
|
||||
| (FontConfig.enableGlyphReplacements ? GLYPH_REPLACEMENTS : 0)
|
||||
| (AngelicaConfig.enableRGBColors ? RGB_COLORS : 0)
|
||||
| (AngelicaConfig.enableGradients ? GRADIENTS : 0)
|
||||
| (AngelicaConfig.enableRainbow ? RAINBOW : 0)
|
||||
| (AngelicaConfig.enableWaveText ? WAVE : 0)
|
||||
| (AngelicaConfig.enableDinnerboneText ? DINNERBONE : 0)
|
||||
| (AngelicaConfig.enableDropShadow ? DROP_SHADOW : 0)
|
||||
| (FontRendering.hexColorResetsStyles() ? HEX_RESETS_STYLES : 0)
|
||||
| (cacheOverride ? CACHE_OVERRIDE : 0);
|
||||
final int slot = cacheOverride ? 1 : 0;
|
||||
final GlyphSettings cached = CURRENT[slot];
|
||||
if (cached != null
|
||||
&& cached.generation == generation
|
||||
&& cached.fontAAMode == fontAAMode
|
||||
&& cached.fontAAStrength == fontAAStrength
|
||||
&& cached.shadowCopies == shadowCopies
|
||||
&& cached.boldCopies == boldCopies
|
||||
&& cached.fontShadowOffset == fontShadowOffset
|
||||
&& cached.fontShadowOffsetUC == fontShadowOffsetUC
|
||||
&& cached.glyphScale == glyphScale
|
||||
&& cached.glyphAspect == glyphAspect
|
||||
&& cached.glyphSpacing == glyphSpacing
|
||||
&& cached.whitespaceScale == whitespaceScale
|
||||
&& cached.customFontScale == customFontScale
|
||||
&& cached.waveAmplitude == waveAmplitude
|
||||
&& cached.flags == flags) {
|
||||
return cached;
|
||||
}
|
||||
final GlyphSettings live = new GlyphSettings(generation, fontAAMode, fontAAStrength, shadowCopies, boldCopies, fontShadowOffset, fontShadowOffsetUC,
|
||||
glyphScale, glyphAspect, glyphSpacing, whitespaceScale, customFontScale, waveAmplitude, flags);
|
||||
CURRENT[slot] = live;
|
||||
return live;
|
||||
}
|
||||
|
||||
public boolean sameMetricsAs(GlyphSettings other) {
|
||||
return generation == other.generation
|
||||
&& glyphScale == other.glyphScale
|
||||
&& glyphAspect == other.glyphAspect
|
||||
&& glyphSpacing == other.glyphSpacing
|
||||
&& whitespaceScale == other.whitespaceScale
|
||||
&& customFontScale == other.customFontScale
|
||||
&& (flags & METRIC_FLAGS) == (other.flags & METRIC_FLAGS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
final class QuadRegionAllocator {
|
||||
|
||||
private int[] freeStart = new int[16];
|
||||
private int[] freeLength = new int[16];
|
||||
private int freeCount;
|
||||
|
||||
private int[] retiredStart = new int[16];
|
||||
private int[] retiredLength = new int[16];
|
||||
private long[] retiredFrame = new long[16];
|
||||
private int retiredCount;
|
||||
|
||||
QuadRegionAllocator(int capacity) {
|
||||
reset(capacity);
|
||||
}
|
||||
|
||||
void reset(int capacity) {
|
||||
freeCount = 0;
|
||||
retiredCount = 0;
|
||||
if (capacity > 0) {
|
||||
freeStart[0] = 0;
|
||||
freeLength[0] = capacity;
|
||||
freeCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int allocate(int quads) {
|
||||
for (int i = 0; i < freeCount; i++) {
|
||||
final int length = freeLength[i];
|
||||
if (length < quads) continue;
|
||||
final int start = freeStart[i];
|
||||
if (length == quads) {
|
||||
System.arraycopy(freeStart, i + 1, freeStart, i, freeCount - i - 1);
|
||||
System.arraycopy(freeLength, i + 1, freeLength, i, freeCount - i - 1);
|
||||
freeCount--;
|
||||
} else {
|
||||
freeStart[i] = start + quads;
|
||||
freeLength[i] = length - quads;
|
||||
}
|
||||
return start;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void retire(int start, int quads, long frame) {
|
||||
if (retiredCount == retiredStart.length) {
|
||||
final int n = retiredCount * 2;
|
||||
retiredStart = Arrays.copyOf(retiredStart, n);
|
||||
retiredLength = Arrays.copyOf(retiredLength, n);
|
||||
retiredFrame = Arrays.copyOf(retiredFrame, n);
|
||||
}
|
||||
retiredStart[retiredCount] = start;
|
||||
retiredLength[retiredCount] = quads;
|
||||
retiredFrame[retiredCount] = frame;
|
||||
retiredCount++;
|
||||
}
|
||||
|
||||
void advance(long frame) {
|
||||
final long reusableUpTo = frame - RenderBackend.MAX_FRAMES_IN_FLIGHT;
|
||||
int kept = 0;
|
||||
for (int i = 0; i < retiredCount; i++) {
|
||||
if (retiredFrame[i] <= reusableUpTo) {
|
||||
free(retiredStart[i], retiredLength[i]);
|
||||
} else {
|
||||
retiredStart[kept] = retiredStart[i];
|
||||
retiredLength[kept] = retiredLength[i];
|
||||
retiredFrame[kept] = retiredFrame[i];
|
||||
kept++;
|
||||
}
|
||||
}
|
||||
retiredCount = kept;
|
||||
}
|
||||
|
||||
private void free(int start, int quads) {
|
||||
int at = 0;
|
||||
while (at < freeCount && freeStart[at] < start) at++;
|
||||
final boolean joinsPrev = at > 0 && freeStart[at - 1] + freeLength[at - 1] == start;
|
||||
final boolean joinsNext = at < freeCount && start + quads == freeStart[at];
|
||||
if (joinsPrev && joinsNext) {
|
||||
freeLength[at - 1] += quads + freeLength[at];
|
||||
System.arraycopy(freeStart, at + 1, freeStart, at, freeCount - at - 1);
|
||||
System.arraycopy(freeLength, at + 1, freeLength, at, freeCount - at - 1);
|
||||
freeCount--;
|
||||
} else if (joinsPrev) {
|
||||
freeLength[at - 1] += quads;
|
||||
} else if (joinsNext) {
|
||||
freeStart[at] = start;
|
||||
freeLength[at] += quads;
|
||||
} else {
|
||||
if (freeCount == freeStart.length) {
|
||||
freeStart = Arrays.copyOf(freeStart, freeCount * 2);
|
||||
freeLength = Arrays.copyOf(freeLength, freeCount * 2);
|
||||
}
|
||||
System.arraycopy(freeStart, at, freeStart, at + 1, freeCount - at);
|
||||
System.arraycopy(freeLength, at, freeLength, at + 1, freeCount - at);
|
||||
freeStart[at] = start;
|
||||
freeLength[at] = quads;
|
||||
freeCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.FeedbackManager;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
final class RetainedTextStore {
|
||||
|
||||
private static final int INITIAL_QUADS = 4096;
|
||||
private static final long EVICT_AFTER_NANOS = 5_000_000_000L;
|
||||
|
||||
// Retired regions and buffers wait MAX_FRAMES_IN_FLIGHT frames: SDL-GPU runs a frame's uploads before its draws, so reusing one sooner overwrites bytes an in-flight draw still reads.
|
||||
private static final QuadRegionAllocator allocator = new QuadRegionAllocator(0);
|
||||
private static final ObjectArrayList<TextMesh> resident = new ObjectArrayList<>();
|
||||
private static int buffer;
|
||||
private static int capacityQuads = INITIAL_QUADS;
|
||||
private static int generation;
|
||||
private static long frame;
|
||||
private static long frameNanos = System.nanoTime();
|
||||
private static int[] retiredBuffers = new int[4];
|
||||
private static long[] retiredBufferFrames = new long[4];
|
||||
private static int retiredBufferCount;
|
||||
|
||||
private RetainedTextStore() {}
|
||||
|
||||
static int buffer() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
static boolean ensure(TextMesh mesh) {
|
||||
if (!GLStateManager.isMainThread() || mesh.quadCount > BatchingFontRenderer.QUAD_BUDGET || GLStateManager.isRecordingDisplayList() || FeedbackManager.isFeedbackMode()) {
|
||||
return false;
|
||||
}
|
||||
if (mesh.regionStart >= 0 && mesh.regionGeneration == generation) {
|
||||
mesh.lastUsedNanos = frameNanos;
|
||||
return true;
|
||||
}
|
||||
final int quads = mesh.quadCount;
|
||||
if (buffer == 0) {
|
||||
create(Math.max(capacityQuads, quads));
|
||||
}
|
||||
int start = allocator.allocate(quads);
|
||||
if (start < 0) {
|
||||
int capacity = capacityQuads * 2;
|
||||
while (capacity < quads) capacity *= 2;
|
||||
retireBuffer();
|
||||
create(capacity);
|
||||
start = allocator.allocate(quads);
|
||||
}
|
||||
upload(mesh, start);
|
||||
mesh.regionStart = start;
|
||||
mesh.regionQuads = quads;
|
||||
mesh.regionGeneration = generation;
|
||||
mesh.lastUsedNanos = frameNanos;
|
||||
resident.add(mesh);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void release(TextMesh mesh) {
|
||||
if (mesh.regionStart >= 0 && mesh.regionGeneration == generation) {
|
||||
final int i = resident.indexOf(mesh);
|
||||
if (i >= 0) {
|
||||
evict(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
mesh.regionStart = -1;
|
||||
mesh.regionQuads = 0;
|
||||
}
|
||||
|
||||
static void endFrame() {
|
||||
frame++;
|
||||
final long now = System.nanoTime();
|
||||
frameNanos = now;
|
||||
allocator.advance(frame);
|
||||
if (retiredBufferCount > 0) {
|
||||
deleteExpiredBuffers();
|
||||
}
|
||||
if (resident.isEmpty()) {
|
||||
retireBuffer();
|
||||
return;
|
||||
}
|
||||
for (int i = resident.size() - 1; i >= 0; i--) {
|
||||
if (now - resident.get(i).lastUsedNanos > EVICT_AFTER_NANOS) {
|
||||
evict(i);
|
||||
}
|
||||
}
|
||||
if (resident.isEmpty()) {
|
||||
retireBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
private static void evict(int index) {
|
||||
final TextMesh mesh = resident.get(index);
|
||||
allocator.retire(mesh.regionStart, mesh.regionQuads, frame);
|
||||
final TextMesh last = resident.pop();
|
||||
if (index < resident.size()) {
|
||||
resident.set(index, last);
|
||||
}
|
||||
mesh.regionStart = -1;
|
||||
mesh.regionQuads = 0;
|
||||
}
|
||||
|
||||
private static void create(int capacity) {
|
||||
final int previous = GLStateManager.ctx().boundVBO;
|
||||
buffer = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, buffer);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, (long) capacity * BatchingFontRenderer.QUAD_BYTES, GL15.GL_DYNAMIC_DRAW);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, previous);
|
||||
capacityQuads = capacity;
|
||||
allocator.reset(capacity);
|
||||
resident.clear();
|
||||
generation++;
|
||||
}
|
||||
|
||||
private static void upload(TextMesh mesh, int start) {
|
||||
final int previous = GLStateManager.ctx().boundVBO;
|
||||
final ByteBuffer vertices = mesh.vertices;
|
||||
vertices.position(0);
|
||||
vertices.limit(mesh.quadCount * BatchingFontRenderer.QUAD_BYTES);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, buffer);
|
||||
GLStateManager.glBufferSubData(GL15.GL_ARRAY_BUFFER, (long) start * BatchingFontRenderer.QUAD_BYTES, vertices);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, previous);
|
||||
vertices.clear();
|
||||
}
|
||||
|
||||
private static void retireBuffer() {
|
||||
if (buffer == 0) return;
|
||||
if (retiredBufferCount == retiredBuffers.length) {
|
||||
retiredBuffers = Arrays.copyOf(retiredBuffers, retiredBufferCount * 2);
|
||||
retiredBufferFrames = Arrays.copyOf(retiredBufferFrames, retiredBufferCount * 2);
|
||||
}
|
||||
retiredBuffers[retiredBufferCount] = buffer;
|
||||
retiredBufferFrames[retiredBufferCount] = frame;
|
||||
retiredBufferCount++;
|
||||
buffer = 0;
|
||||
resident.clear();
|
||||
generation++;
|
||||
}
|
||||
|
||||
private static void deleteExpiredBuffers() {
|
||||
final long deletableUpTo = frame - RenderBackend.MAX_FRAMES_IN_FLIGHT;
|
||||
int kept = 0;
|
||||
for (int i = 0; i < retiredBufferCount; i++) {
|
||||
final int id = retiredBuffers[i];
|
||||
if (retiredBufferFrames[i] <= deletableUpTo) {
|
||||
BatchingFontRenderer.forgetVertexSource(id);
|
||||
GLStateManager.glDeleteBuffers(id);
|
||||
} else {
|
||||
retiredBuffers[kept] = id;
|
||||
retiredBufferFrames[kept] = retiredBufferFrames[i];
|
||||
kept++;
|
||||
}
|
||||
}
|
||||
retiredBufferCount = kept;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLContextState;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.states.ColorMask;
|
||||
import com.gtnewhorizons.angelica.glsm.states.PolygonState;
|
||||
@@ -28,23 +29,23 @@ final class TextDrawState {
|
||||
float offsetUnits;
|
||||
float alphaRef;
|
||||
|
||||
void captureLive() {
|
||||
depthTest = GLStateManager.getDepthTest().isEnabled();
|
||||
depthFunc = GLStateManager.getDepthState().getFunc();
|
||||
depthMask = GLStateManager.isEffectiveDepthMaskEnabled();
|
||||
final ColorMask colorMask = GLStateManager.getEffectiveColorMask(colorMaskScratch);
|
||||
void captureLive(GLContextState ctx) {
|
||||
depthTest = ctx.depthTest.isEnabled();
|
||||
depthFunc = ctx.depthState.getFunc();
|
||||
depthMask = ctx.depthState.isEffectiveMaskEnabled();
|
||||
final ColorMask colorMask = ctx.colorMask.readEffective(colorMaskScratch);
|
||||
colorRed = colorMask.red;
|
||||
colorGreen = colorMask.green;
|
||||
colorBlue = colorMask.blue;
|
||||
colorAlpha = colorMask.alpha;
|
||||
final PolygonState polygon = GLStateManager.getPolygonState();
|
||||
cull = GLStateManager.getCullState().isEnabled();
|
||||
final PolygonState polygon = ctx.polygonState;
|
||||
cull = ctx.cullState.isEnabled();
|
||||
cullFace = polygon.getCullFaceMode();
|
||||
frontFace = polygon.getFrontFace();
|
||||
polygonOffset = GLStateManager.getPolygonOffsetFillState().isEnabled();
|
||||
polygonOffset = ctx.polygonOffsetFillState.isEnabled();
|
||||
offsetFactor = polygon.getOffsetFactor();
|
||||
offsetUnits = polygon.getOffsetUnits();
|
||||
alphaRef = GLStateManager.getAlphaState().getReference();
|
||||
alphaRef = ctx.alphaState.getReference();
|
||||
}
|
||||
|
||||
boolean sameAs(TextDrawState other) {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memRealloc;
|
||||
|
||||
public final class TextMesh {
|
||||
|
||||
private static final int INITIAL_COMMANDS = 8;
|
||||
|
||||
GlyphSettings settings;
|
||||
ByteBuffer vertices;
|
||||
long address;
|
||||
int quadCount;
|
||||
int[] commandStart = new int[INITIAL_COMMANDS];
|
||||
int[] commandCount = new int[INITIAL_COMMANDS];
|
||||
int[] commandTexture = new int[INITIAL_COMMANDS];
|
||||
int[] commandLayer = new int[INITIAL_COMMANDS];
|
||||
boolean[] commandUnicode = new boolean[INITIAL_COMMANDS];
|
||||
int commands;
|
||||
boolean captured;
|
||||
boolean animated;
|
||||
int regionStart = -1;
|
||||
int regionQuads;
|
||||
int regionGeneration;
|
||||
long lastUsedNanos;
|
||||
|
||||
public boolean isValid(GlyphSettings live) {
|
||||
return captured && !BatchingFontRenderer.recolorActive() && live.equals(settings);
|
||||
}
|
||||
|
||||
public boolean isAnimated() {
|
||||
return animated;
|
||||
}
|
||||
|
||||
void reserveVertexBytes(int bytes) {
|
||||
if (vertices == null) {
|
||||
vertices = memAlloc(Math.max(bytes, 1));
|
||||
} else if (vertices.capacity() < bytes) {
|
||||
vertices = memRealloc(vertices, Math.max(bytes, vertices.capacity() * 2));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
address = memAddress0(vertices);
|
||||
}
|
||||
|
||||
public void free() {
|
||||
RetainedTextStore.release(this);
|
||||
memFree(vertices);
|
||||
vertices = null;
|
||||
address = 0L;
|
||||
captured = false;
|
||||
}
|
||||
|
||||
void addCommand(int start, int count, int texture, boolean unicode, int layer) {
|
||||
if (commands == commandStart.length) {
|
||||
final int n = commands * 2;
|
||||
commandStart = Arrays.copyOf(commandStart, n);
|
||||
commandCount = Arrays.copyOf(commandCount, n);
|
||||
commandTexture = Arrays.copyOf(commandTexture, n);
|
||||
commandLayer = Arrays.copyOf(commandLayer, n);
|
||||
commandUnicode = Arrays.copyOf(commandUnicode, n);
|
||||
}
|
||||
commandStart[commands] = start;
|
||||
commandCount[commands] = count;
|
||||
commandTexture[commands] = texture;
|
||||
commandLayer[commands] = layer;
|
||||
commandUnicode[commands] = unicode;
|
||||
commands++;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,13 @@ package com.gtnewhorizons.angelica.commands;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.debug.ChunkDebugMinimap;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugEntryDef;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugFactImpl;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreen;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreenEntries;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreenEntryList;
|
||||
import com.gtnewhorizons.angelica.debug.screen.entries.AngelicaEntries;
|
||||
import com.gtnewhorizons.angelica.debug.flyby.FlybyRoute;
|
||||
import com.gtnewhorizons.angelica.debug.flyby.FlybyRunner;
|
||||
import com.gtnewhorizons.angelica.debug.profiling.AsprofRecorder;
|
||||
@@ -13,7 +19,6 @@ import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.TracyBackend;
|
||||
import com.gtnewhorizons.angelica.rendering.IsbrhTestRenderer;
|
||||
import com.gtnewhorizons.angelica.rendering.RenderRecovery;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasDebugScreenHandler;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasWorldRenderer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.command.CommandBase;
|
||||
@@ -38,7 +43,7 @@ public class AngelicaCommand extends CommandBase {
|
||||
final Map<String, String> help = new LinkedHashMap<>();
|
||||
if (SystemProperties.debugTooling()) {
|
||||
help.put("wireframe", helpLine("wireframe", "Toggle wireframe rendering"));
|
||||
help.put("fog", helpLine("fog", "Toggle fog debug on F3"));
|
||||
help.put("fog", helpLine("fog", "Toggle always-on fog debug info"));
|
||||
help.put("minimap", helpLine("minimap", "Toggle chunk debug overlay"));
|
||||
help.put("flyby", helpLine("flyby <" + FlybyRoute.ids() + ">", "Run a deterministic benchmark route"));
|
||||
help.put("profile", helpLine("profile <start|stop|status>", "Control async-profiler (JFR) recording"));
|
||||
@@ -154,19 +159,33 @@ public class AngelicaCommand extends CommandBase {
|
||||
}
|
||||
|
||||
private void handleFog(ICommandSender sender) {
|
||||
CeleritasDebugScreenHandler.showFogDebug = !CeleritasDebugScreenHandler.showFogDebug;
|
||||
final String f3State = CeleritasDebugScreenHandler.showFogDebug ? "ON" : "OFF";
|
||||
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + "Fog debug (F3): " + f3State));
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + " " + CeleritasDebugScreenHandler.getFogDebugString()));
|
||||
if (debugScreenUnavailable(sender)) return;
|
||||
final boolean on = toggleAlways(DebugScreenEntries.ANGELICA_FOG);
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + "Fog debug: " + (on ? "ON" : "OFF")));
|
||||
final StringBuilder fogText = new StringBuilder();
|
||||
AngelicaEntries.writeFog(new DebugFactImpl().intoPlain(fogText));
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + " " + fogText));
|
||||
}
|
||||
|
||||
private void handleMinimap(ICommandSender sender) {
|
||||
ChunkDebugMinimap.toggle();
|
||||
final String state = ChunkDebugMinimap.isEnabled() ? "ON" : "OFF";
|
||||
if (debugScreenUnavailable(sender)) return;
|
||||
final String state = toggleAlways(DebugScreenEntries.ANGELICA_CHUNK_MINIMAP) ? "ON" : "OFF";
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + "Chunk debug minimap: " + state));
|
||||
}
|
||||
|
||||
private static boolean debugScreenUnavailable(ICommandSender sender) {
|
||||
if (DebugScreen.ENTRIES != null) return false;
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.RED + "The debug screen failed to start; see the log"));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean toggleAlways(DebugEntryDef def) {
|
||||
final DebugScreenEntryList entries = DebugScreen.ENTRIES;
|
||||
final boolean on = entries.effectiveStatus(def) != DebugScreenEntryStatus.ALWAYS_ON;
|
||||
entries.overrideForSession(def, on ? DebugScreenEntryStatus.ALWAYS_ON : DebugScreenEntryStatus.NEVER);
|
||||
return on;
|
||||
}
|
||||
|
||||
private void handleCrashTest(ICommandSender sender, String[] args) {
|
||||
if (args.length >= 2 && "isbrh".equalsIgnoreCase(args[1])) {
|
||||
IsbrhTestRenderer.armCrashTest();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.common;
|
||||
|
||||
import com.gtnewhorizons.angelica.rendering.TextureAtlasType;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import net.minecraft.block.Block;
|
||||
@@ -39,7 +40,7 @@ public class BlockError extends Block {
|
||||
@SubscribeEvent
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onTextureStitch(TextureStitchEvent.Pre event) {
|
||||
if (event.map.getTextureType() == 0) {
|
||||
if (event.map.getTextureType() == TextureAtlasType.BLOCK) {
|
||||
for (int i = 0; i < icons.length; i++) {
|
||||
icons[i] = event.map.registerIcon("angelica:error_block_" + i);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.gtnewhorizons.angelica.compat.debug;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenApi;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.world.World;
|
||||
import org.lwjgl.opengl.ContextCapabilities;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import vazkii.botania.client.fx.ParticleRenderDispatcher;
|
||||
import vazkii.botania.common.core.handler.ConfigHandler;
|
||||
import vazkii.botania.common.core.handler.ManaNetworkHandler;
|
||||
import vazkii.botania.common.lib.LibMisc;
|
||||
|
||||
public final class BotaniaDebugEntry implements DebugScreenEntry {
|
||||
|
||||
static final String GROUP_ID = "botania:botania";
|
||||
static final String ENTRY_ID = "botania:debug";
|
||||
|
||||
private static DebugEntryHandle handle;
|
||||
|
||||
private final DebugGroupHandle group;
|
||||
private final String version = displayVersion(runtimeVersion());
|
||||
private int collectors;
|
||||
private int pools;
|
||||
private String glVersion;
|
||||
private String glRenderer;
|
||||
private String glslVersion;
|
||||
|
||||
private BotaniaDebugEntry(DebugGroupHandle group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
static void register() {
|
||||
handle = DebugCompat.register(GROUP_ID, ENTRY_ID, "Botania", 0x00AA00, entry -> new BotaniaDebugEntry(DebugScreenApi.group(GROUP_ID)));
|
||||
}
|
||||
|
||||
public static boolean active() {
|
||||
return DebugCompat.active(handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (!ConfigHandler.showF3Debug) return;
|
||||
if (context.isSampleDue()) {
|
||||
final World world = context.world();
|
||||
collectors = ManaNetworkHandler.instance.getAllCollectorsInWorld(world).size();
|
||||
pools = ManaNetworkHandler.instance.getAllPoolsInWorld(world).size();
|
||||
}
|
||||
writeFacts(displayer, group, ParticleRenderDispatcher.sparkleFxCount, ParticleRenderDispatcher.fakeSparkleFxCount, ParticleRenderDispatcher.wispFxCount, ParticleRenderDispatcher.depthIgnoringWispFxCount, ParticleRenderDispatcher.lightningCount, collectors, pools, version);
|
||||
if (!GuiScreen.isCtrlKeyDown() || !GuiScreen.isShiftKeyDown()) {
|
||||
writeContextHint(displayer, group, Minecraft.isRunningOnMac);
|
||||
return;
|
||||
}
|
||||
if (glVersion == null) {
|
||||
glVersion = GLStateManager.glGetString(GL11.GL_VERSION);
|
||||
glRenderer = GLStateManager.glGetString(GL11.GL_RENDERER);
|
||||
glslVersion = GLStateManager.glGetString(GL20.GL_SHADING_LANGUAGE_VERSION);
|
||||
}
|
||||
final ContextCapabilities caps = GLStateManager.capabilities;
|
||||
writeContext(displayer, group, ConfigHandler.useShaders, ConfigHandler.glSecondaryTextureUnit, glVersion, glRenderer, glslVersion, GLStateManager.MAX_TEXTURE_UNITS, caps.GL_ARB_multitexture, caps.GL_ARB_texture_non_power_of_two, caps.OpenGL13);
|
||||
}
|
||||
|
||||
private static String runtimeVersion() {
|
||||
try {
|
||||
return (String) LibMisc.class.getField("VERSION").get(null);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static String displayVersion(String version) {
|
||||
return version == null || version.contains("GRADLE") ? "N/A" : version;
|
||||
}
|
||||
|
||||
static void writeFacts(DebugScreenDisplayer displayer, DebugGroupHandle group, int sparkles, int fakeSparkles, int wisps, int depthIgnoringWisps, int lightning, int collectors, int pools, String version) {
|
||||
displayer.fact(group, "Sparkles").value(sparkles).text(" (").value(fakeSparkles).text(" fake)");
|
||||
displayer.fact(group, "Wisps").value(wisps).text(" (").value(depthIgnoringWisps).text(" depth-ignoring)");
|
||||
displayer.fact(group, "Lightning").value(lightning);
|
||||
displayer.fact(group, "Mana Network").value(collectors).text(" collectors, ").value(pools).text(" pools");
|
||||
displayer.fact(group, "Version").value(version);
|
||||
}
|
||||
|
||||
static void writeContextHint(DebugScreenDisplayer displayer, DebugGroupHandle group, boolean mac) {
|
||||
displayer.line(group).text(mac ? "SHIFT+CMD for context" : "SHIFT+CTRL for context");
|
||||
}
|
||||
|
||||
static void writeContext(DebugScreenDisplayer displayer, DebugGroupHandle group, boolean useShaders, int secondaryUnit, String glVersion, String glRenderer, String glslVersion, int maxTextureUnits, boolean multitexture, boolean nonPowerOfTwo, boolean openGl13) {
|
||||
displayer.line(group).text("Config Context");
|
||||
displayer.fact(group, "shaders.enabled").value(String.valueOf(useShaders));
|
||||
displayer.fact(group, "shaders.secondaryUnit").value(secondaryUnit);
|
||||
displayer.line(group).text("OpenGL Context");
|
||||
displayer.fact(group, "GL_VERSION").value(String.valueOf(glVersion));
|
||||
displayer.fact(group, "GL_RENDERER").value(String.valueOf(glRenderer));
|
||||
displayer.fact(group, "GL_SHADING_LANGUAGE_VERSION").value(String.valueOf(glslVersion));
|
||||
displayer.fact(group, "GL_MAX_TEXTURE_IMAGE_UNITS_ARB").value(maxTextureUnits);
|
||||
displayer.fact(group, "GL_ARB_multitexture").value(String.valueOf(multitexture));
|
||||
displayer.fact(group, "GL_ARB_texture_non_power_of_two").value(String.valueOf(nonPowerOfTwo));
|
||||
displayer.fact(group, "OpenGL13").value(String.valueOf(openGl13));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.gtnewhorizons.angelica.compat.debug;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryCategory;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntrySpec;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntrySpec.DebugEntryFactory;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupSpec;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenApi;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugRegistry;
|
||||
import cpw.mods.fml.common.Loader;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public final class DebugCompat {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("Angelica");
|
||||
|
||||
private DebugCompat() {}
|
||||
|
||||
public static void registerMissing() {
|
||||
if (Loader.isModLoaded("Botania")) registerMissing("botania", BotaniaDebugEntry::register);
|
||||
if (Loader.isModLoaded("journeymap")) registerMissing("journeymap", JourneyMapDebugEntry::register);
|
||||
if (Loader.isModLoaded("hodgepodge")) registerMissing("hodgepodge", HodgepodgeDebugEntry::register);
|
||||
}
|
||||
|
||||
static void registerMissing(String namespace, Runnable register) {
|
||||
if (DebugRegistry.MAIN.hasNamespace(namespace)) return;
|
||||
try {
|
||||
register.run();
|
||||
} catch (RuntimeException | LinkageError e) {
|
||||
LOGGER.error("Debug screen compat entry for {} failed to register", namespace, e);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean active(DebugEntryHandle handle) {
|
||||
return handle != null && !handle.isFailed();
|
||||
}
|
||||
|
||||
static DebugEntryHandle register(String groupId, String entryId, String name, int accent, DebugEntryFactory factory) {
|
||||
DebugScreenApi.registerGroup(new DebugGroupSpec(groupId).title(name).accent(accent).side(DebugGroupHandle.LEFT));
|
||||
return DebugScreenApi.registerEntry(new DebugEntrySpec(entryId, DebugEntryCategory.SCREEN_TEXT)
|
||||
.name(name)
|
||||
.defaults(DebugScreenEntryStatus.IN_OVERLAY, DebugScreenEntryStatus.NEVER)
|
||||
.text(factory));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.gtnewhorizons.angelica.compat.debug;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenApi;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.mitchej123.hodgepodge.config.DebugConfig;
|
||||
import com.mitchej123.hodgepodge.mixins.hooks.ChunkGenScheduler;
|
||||
import com.mitchej123.hodgepodge.mixins.hooks.ChunkGenStats;
|
||||
import net.minecraftforge.classloading.FMLForgePlugin;
|
||||
|
||||
public final class HodgepodgeDebugEntry implements DebugScreenEntry {
|
||||
|
||||
static final String GROUP_ID = "hodgepodge:chunkgen";
|
||||
static final String ENTRY_ID = "hodgepodge:chunkgen";
|
||||
|
||||
private static DebugEntryHandle handle;
|
||||
|
||||
private final DebugGroupHandle group;
|
||||
private boolean scheduled;
|
||||
private double budgetUsedMs;
|
||||
private double budgetAvailMs;
|
||||
private double terrainGenEmaMs;
|
||||
private double populateEmaMs;
|
||||
private int genQueue;
|
||||
private int popQueue;
|
||||
private int pipelineBacklog;
|
||||
private int terrainGenCount;
|
||||
private int popCount;
|
||||
private int blockedLoads;
|
||||
private int stuckCount;
|
||||
private int neighborFills;
|
||||
private double debtMs;
|
||||
|
||||
private HodgepodgeDebugEntry(DebugGroupHandle group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
static void register() {
|
||||
if (!DebugConfig.showChunkGenDebug && FMLForgePlugin.RUNTIME_DEOBF) return;
|
||||
handle = DebugCompat.register(GROUP_ID, ENTRY_ID, "Chunk Generation", 0xFFAA00, entry -> new HodgepodgeDebugEntry(DebugScreenApi.group(GROUP_ID)));
|
||||
}
|
||||
|
||||
public static boolean active() {
|
||||
return DebugCompat.active(handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (context.isSampleDue()) sample(context);
|
||||
if (!scheduled) return;
|
||||
writeFacts(displayer, group, budgetUsedMs, budgetAvailMs, terrainGenEmaMs, populateEmaMs, genQueue, popQueue, pipelineBacklog, terrainGenCount, popCount, blockedLoads, stuckCount, neighborFills, debtMs);
|
||||
}
|
||||
|
||||
private void sample(DebugContext context) {
|
||||
final ChunkGenScheduler scheduler = ChunkGenScheduler.getForDimension(context.player().dimension);
|
||||
scheduled = scheduler != null;
|
||||
if (scheduler == null) return;
|
||||
final ChunkGenStats stats = scheduler.getStats();
|
||||
budgetUsedMs = stats.getSnapBudgetUsedMs();
|
||||
budgetAvailMs = stats.getSnapBudgetAvailMs();
|
||||
terrainGenEmaMs = stats.getTerrainGenEmaMs();
|
||||
populateEmaMs = stats.getPopulateEmaMs();
|
||||
genQueue = stats.getSnapGenQueue();
|
||||
popQueue = stats.getSnapPopQueue();
|
||||
pipelineBacklog = stats.getSnapPipelineBacklog();
|
||||
terrainGenCount = stats.getSnapTerrainGenCount();
|
||||
popCount = stats.getSnapPopCount();
|
||||
blockedLoads = stats.getSnapBlockedLoads();
|
||||
stuckCount = stats.getSnapStuckCount();
|
||||
neighborFills = stats.getSnapNeighborFills();
|
||||
debtMs = stats.getDebtMs();
|
||||
}
|
||||
|
||||
static void writeFacts(DebugScreenDisplayer displayer, DebugGroupHandle group, double budgetUsedMs, double budgetAvailMs, double terrainGenEmaMs, double populateEmaMs, int genQueue, int popQueue, int pipelineBacklog, int terrainGenCount, int popCount, int blockedLoads, int stuckCount, int neighborFills, double debtMs) {
|
||||
displayer.fact(group, "Budget").value(budgetUsedMs, 1).text(" / ").value(budgetAvailMs, 1).text(" ms");
|
||||
displayer.fact(group, "Average").text("terrain ").value(terrainGenEmaMs, 1).text(" ms, populate ").value(populateEmaMs, 1).text(" ms");
|
||||
displayer.fact(group, "Queues").text("gen ").value(genQueue).text(", populate ").value(popQueue).text(", pipeline ").value(pipelineBacklog);
|
||||
displayer.fact(group, "Active").text("gen ").value(terrainGenCount).text(", populate ").value(popCount);
|
||||
displayer.fact(group, "Blocked").value(blockedLoads).text(" blocked, ").value(stuckCount).text(" stuck, ").value(neighborFills).text(" fills");
|
||||
displayer.fact(group, "Debt").value(debtMs, 1).text(" ms");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.gtnewhorizons.angelica.compat.debug;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugFactLines;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenApi;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class JourneyMapDebugEntry implements DebugScreenEntry {
|
||||
|
||||
static final String GROUP_ID = "journeymap:journeymap";
|
||||
static final String ENTRY_ID = "journeymap:debug";
|
||||
|
||||
private static final String MAPPING_DISABLED_KEY = "jm.common.enable_mapping_false_text";
|
||||
private static final long REPORT_INTERVAL_NS = TimeUnit.SECONDS.toNanos(3);
|
||||
|
||||
private static DebugEntryHandle handle;
|
||||
|
||||
private final DebugGroupHandle group;
|
||||
private final DebugFactLines facts = new DebugFactLines();
|
||||
private MethodHandle stats;
|
||||
private MethodHandle mappingEnabled;
|
||||
private MethodHandle timerReport;
|
||||
private List<String> report = Collections.emptyList();
|
||||
private long reportAt;
|
||||
private boolean reported;
|
||||
|
||||
private JourneyMapDebugEntry(DebugGroupHandle group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
static void register() {
|
||||
handle = DebugCompat.register(GROUP_ID, ENTRY_ID, "JourneyMap", 0x55FFFF, entry -> new JourneyMapDebugEntry(DebugScreenApi.group(GROUP_ID)));
|
||||
}
|
||||
|
||||
public static boolean active() {
|
||||
return DebugCompat.active(handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (context.isSampleDue()) sample(context.nanoTime());
|
||||
facts.emit(displayer, group);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void sample(long now) {
|
||||
try {
|
||||
if (stats == null) resolve();
|
||||
final boolean mapping = ((AtomicBoolean) mappingEnabled.invokeExact()).get();
|
||||
final boolean profiler = Minecraft.getMinecraft().gameSettings.showDebugProfilerChart;
|
||||
if (profiler && (!reported || now - reportAt > REPORT_INTERVAL_NS)) {
|
||||
report = (List<String>) timerReport.invokeExact("", "");
|
||||
reportAt = now;
|
||||
reported = true;
|
||||
}
|
||||
writeFacts(facts, mapping ? (String[]) stats.invokeExact() : null, mapping ? null : I18n.format(MAPPING_DISABLED_KEY), profiler ? report : null);
|
||||
} catch (RuntimeException | Error e) {
|
||||
throw e;
|
||||
} catch (Throwable t) {
|
||||
throw new IllegalStateException("JourneyMap debug stats are unavailable", t);
|
||||
}
|
||||
}
|
||||
|
||||
private void resolve() throws ReflectiveOperationException {
|
||||
final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
|
||||
final Class<?> properties = Class.forName("journeymap.client.properties.CoreProperties");
|
||||
final MethodHandle coreProperties = lookup.findStatic(Class.forName("journeymap.client.JourneymapClient"), "getCoreProperties", MethodType.methodType(properties));
|
||||
mappingEnabled = MethodHandles.filterReturnValue(coreProperties, lookup.findGetter(properties, "mappingEnabled", AtomicBoolean.class));
|
||||
timerReport = lookup.findStatic(Class.forName("journeymap.client.log.StatTimer"), "getReportByTotalTime", MethodType.methodType(List.class, String.class, String.class));
|
||||
stats = lookup.findStatic(Class.forName("journeymap.client.task.multi.MapPlayerTask"), "getDebugStats", MethodType.methodType(String[].class));
|
||||
}
|
||||
|
||||
static void writeFacts(DebugFactLines facts, String[] stats, String mappingDisabled, List<String> report) {
|
||||
facts.begin();
|
||||
if (mappingDisabled != null) facts.line(mappingDisabled);
|
||||
if (stats != null) {
|
||||
for (int i = 0; i < stats.length; i++) {
|
||||
final String line = stats[i];
|
||||
if (line != null) facts.split(line);
|
||||
}
|
||||
}
|
||||
if (report != null) {
|
||||
for (int i = 0, n = report.size(); i < n; i++) {
|
||||
final String line = report.get(i);
|
||||
if (line != null) facts.line(line);
|
||||
}
|
||||
}
|
||||
facts.end();
|
||||
}
|
||||
}
|
||||
@@ -108,10 +108,6 @@ public class AngelicaConfig {
|
||||
@Config.DefaultBoolean(true)
|
||||
public static boolean modernizeF3Screen;
|
||||
|
||||
@Config.Comment("Show block registry name and meta value in F3, similar to 1.8+. [From ArchaicFix]")
|
||||
@Config.DefaultBoolean(true)
|
||||
public static boolean showBlockDebugInfo;
|
||||
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.Comment("Hide downloading terrain screen. [From ArchaicFix]")
|
||||
public static boolean hideDownloadingTerrainScreen;
|
||||
@@ -206,6 +202,11 @@ public class AngelicaConfig {
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean enableTESRProviderDispatch;
|
||||
|
||||
@Config.Comment("Cache the lightmap the tile-entity dispatcher looks up per tile per frame, refreshing it once a tick")
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean enableTESRLightCache;
|
||||
|
||||
@Config.Comment("Enable full RGB color support (16.7M colors) using &#RRGGBB syntax in text")
|
||||
@Config.DefaultBoolean(true)
|
||||
public static boolean enableRGBColors;
|
||||
@@ -455,13 +456,14 @@ public class AngelicaConfig {
|
||||
@Config.DefaultBoolean(false)
|
||||
public static boolean useVanillaChunkTracking;
|
||||
|
||||
@Config.Comment("Disables additional F3 information added by Angelica.")
|
||||
@Config.DefaultBoolean(false)
|
||||
public static boolean disableF3Additions;
|
||||
|
||||
@Config.Comment("Shows developer counters (FFP, streaming, TESR, transfer) on the F3 screen. Toggle in game with F3+V.")
|
||||
@Config.DefaultBoolean(false)
|
||||
public static boolean verboseF3;
|
||||
@Config.Comment({"Size of the F3 text.",
|
||||
"-1 = same as your GUI Scale (default, the size vanilla F3 uses)",
|
||||
"0 = Auto: half the largest size that fits the window",
|
||||
"1-16 = that size",
|
||||
"F3 is never drawn larger than your GUI Scale."})
|
||||
@Config.DefaultInt(-1)
|
||||
@Config.RangeInt(min = -1, max = 16)
|
||||
public static int debugGuiScale;
|
||||
|
||||
@Config.Comment("Replaces various FFP uploads with statically allocated VBO's.")
|
||||
@Config.DefaultBoolean(true)
|
||||
|
||||
@@ -50,4 +50,13 @@ public class CompatConfig {
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean fixNTMSpace;
|
||||
|
||||
@Config.Comment({"Rules for F3 lines that other mods add without Angelica's F3 API, one rule per entry.",
|
||||
"A rule matches lines that start with the quoted text (color codes ignored), in the left or right column.",
|
||||
" drop left \"Text: \" hides the line",
|
||||
" fact left \"Text: \" group=<panel id> moves the line into that F3 panel; add name=\"Label\" to rename it",
|
||||
"Panel ids are listed under Panels in the F3+F6 screen; entry ids show when you hover an entry there.",
|
||||
"Add when=<entry id> to apply a rule only while that F3 entry is enabled."})
|
||||
@Config.DefaultStringList({})
|
||||
public static String[] debugLineRules;
|
||||
|
||||
}
|
||||
@@ -22,33 +22,48 @@
|
||||
|
||||
package com.gtnewhorizons.angelica.debug;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugDrawScope;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugGeometry;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugProgram;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugPrograms;
|
||||
import com.gtnewhorizons.angelica.debug.render.NativeScratch;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.AngelicaRenderSectionManager;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasWorldRenderer;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import lombok.Getter;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ChunkProviderClient;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.world.chunk.EmptyChunk;
|
||||
import net.minecraftforge.client.event.RenderGameOverlayEvent;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
/**
|
||||
* Debug overlay that renders a minimap showing chunk loading and rendering state. Toggle via /angelica minimap
|
||||
* Debug overlay that renders a minimap showing chunk loading and rendering state.
|
||||
*/
|
||||
public class ChunkDebugMinimap {
|
||||
private static final ChunkDebugMinimap INSTANCE = new ChunkDebugMinimap();
|
||||
@Getter
|
||||
private static volatile boolean enabled = false;
|
||||
|
||||
private static final int RANGE = 32; // Chunks in each direction from player
|
||||
|
||||
public static synchronized void toggle() {
|
||||
if (enabled) disable();
|
||||
else enable();
|
||||
}
|
||||
private static final int FLAGS = VertexFlags.COLOR_BIT;
|
||||
private static final VertexFormat FORMAT = DefaultVertexFormat.ALL_FORMATS[FLAGS];
|
||||
private static final int STRIDE = FORMAT.getVertexSize();
|
||||
private static final int COLOR_OFFSET = DebugGeometry.offsetOf(FORMAT, VertexFormatElement.Usage.COLOR);
|
||||
private static final int CHUNKS = RANGE * 2 * RANGE * 2;
|
||||
private static final int ALPHA = (int) (0.8F * 255.0F);
|
||||
private static final int BYTES = CHUNKS * 4 * STRIDE;
|
||||
private static final ChunkDebugMinimap INSTANCE = new ChunkDebugMinimap();
|
||||
private static volatile boolean enabled = false;
|
||||
|
||||
private final DebugDrawScope scope = new DebugDrawScope();
|
||||
private final NativeScratch scratch = new NativeScratch(BYTES);
|
||||
private final Matrix4f mvp = new Matrix4f();
|
||||
private long cursor;
|
||||
|
||||
public static synchronized void enable() {
|
||||
if (enabled) return;
|
||||
@@ -60,6 +75,7 @@ public class ChunkDebugMinimap {
|
||||
if (!enabled) return;
|
||||
enabled = false;
|
||||
MinecraftForge.EVENT_BUS.unregister(INSTANCE);
|
||||
INSTANCE.scratch.free();
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
@@ -84,39 +100,31 @@ public class ChunkDebugMinimap {
|
||||
final int playerChunkX = ((int) Math.floor(pX)) >> 4;
|
||||
final int playerChunkZ = ((int) Math.floor(pZ)) >> 4;
|
||||
|
||||
// Save GL state
|
||||
GLStateManager.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
|
||||
GLStateManager.glPushMatrix();
|
||||
try {
|
||||
// Setup rendering
|
||||
GLStateManager.glScaled(2, 2, 1);
|
||||
GLStateManager.glTranslated(32, 32, 0);
|
||||
scratch.ensure(BYTES);
|
||||
cursor = scratch.address();
|
||||
final ChunkProviderClient chunkProvider = (ChunkProviderClient) mc.theWorld.getChunkProvider();
|
||||
|
||||
GLStateManager.disableLighting();
|
||||
GLStateManager.disableTexture();
|
||||
GLStateManager.disableDepthTest();
|
||||
GLStateManager.enableBlend();
|
||||
GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
final Tessellator tess = Tessellator.instance;
|
||||
tess.startDrawingQuads();
|
||||
|
||||
final ChunkProviderClient chunkProvider = (ChunkProviderClient) mc.theWorld.getChunkProvider();
|
||||
|
||||
for (int x = playerChunkX - RANGE; x < playerChunkX + RANGE; x++) {
|
||||
for (int z = playerChunkZ - RANGE; z < playerChunkZ + RANGE; z++) {
|
||||
drawChunk(tess, x, z, playerChunkX, playerChunkZ, manager, chunkProvider);
|
||||
}
|
||||
for (int x = playerChunkX - RANGE; x < playerChunkX + RANGE; x++) {
|
||||
for (int z = playerChunkZ - RANGE; z < playerChunkZ + RANGE; z++) {
|
||||
drawChunk(x, z, playerChunkX, playerChunkZ, manager, chunkProvider);
|
||||
}
|
||||
}
|
||||
|
||||
tess.draw();
|
||||
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), mvp).scale(2, 2, 1).translate(32, 32, 0);
|
||||
|
||||
scope.beginHud();
|
||||
try {
|
||||
final DebugProgram color = DebugPrograms.color();
|
||||
color.use();
|
||||
color.mvp(mvp);
|
||||
final int bytes = (int) (cursor - scratch.address());
|
||||
TessellatorStreamingDrawer.drawPacked(scratch.window(bytes), GL11.GL_QUADS, FLAGS, bytes / STRIDE);
|
||||
} finally {
|
||||
GLStateManager.glPopMatrix();
|
||||
GLStateManager.glPopAttrib();
|
||||
scope.end();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawChunk(Tessellator tess, int chunkX, int chunkZ, int playerChunkX, int playerChunkZ, AngelicaRenderSectionManager manager, ChunkProviderClient chunkProvider) {
|
||||
private void drawChunk(int chunkX, int chunkZ, int playerChunkX, int playerChunkZ, AngelicaRenderSectionManager manager, ChunkProviderClient chunkProvider) {
|
||||
boolean initialized = false;
|
||||
float r = 0, g = 0, b = 0;
|
||||
|
||||
@@ -142,16 +150,23 @@ public class ChunkDebugMinimap {
|
||||
}
|
||||
}
|
||||
|
||||
tess.setColorRGBA_F(r, g, b, 0.8f);
|
||||
final int argb = ALPHA << 24 | (int) (r * 255) << 16 | (int) (g * 255) << 8 | (int) (b * 255);
|
||||
|
||||
final int xStart = chunkX - playerChunkX;
|
||||
final int zStart = chunkZ - playerChunkZ;
|
||||
final int xEnd = xStart + 1;
|
||||
final int zEnd = zStart + 1;
|
||||
|
||||
tess.addVertex(xStart, zStart, 0);
|
||||
tess.addVertex(xStart, zEnd, 0);
|
||||
tess.addVertex(xEnd, zEnd, 0);
|
||||
tess.addVertex(xEnd, zStart, 0);
|
||||
vertex(xStart, zStart, argb);
|
||||
vertex(xStart, zEnd, argb);
|
||||
vertex(xEnd, zEnd, argb);
|
||||
vertex(xEnd, zStart, argb);
|
||||
}
|
||||
|
||||
private void vertex(float x, float y, int argb) {
|
||||
final long p = cursor;
|
||||
DebugGeometry.putPosition(p, x, y, 0.0F);
|
||||
DebugGeometry.putColor(p + COLOR_OFFSET, argb);
|
||||
cursor = p + STRIDE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.gtnewhorizons.angelica.debug;
|
||||
|
||||
import org.lwjgl.input.Keyboard;
|
||||
|
||||
public final class DebugF3Toggle {
|
||||
|
||||
private boolean armed;
|
||||
private boolean comboUsed;
|
||||
private boolean shift;
|
||||
private boolean alt;
|
||||
|
||||
public void arm(boolean shift, boolean alt) {
|
||||
armed = true;
|
||||
comboUsed = false;
|
||||
this.shift = shift;
|
||||
this.alt = alt;
|
||||
}
|
||||
|
||||
public void disarm() {
|
||||
armed = false;
|
||||
comboUsed = false;
|
||||
}
|
||||
|
||||
public boolean isArmed() {
|
||||
return armed;
|
||||
}
|
||||
|
||||
public boolean shift() {
|
||||
return shift;
|
||||
}
|
||||
|
||||
public boolean alt() {
|
||||
return alt;
|
||||
}
|
||||
|
||||
public void onKeyPress(int key) {
|
||||
if (!armed) return;
|
||||
switch (key) {
|
||||
case Keyboard.KEY_F3, Keyboard.KEY_LSHIFT, Keyboard.KEY_RSHIFT, Keyboard.KEY_LMENU, Keyboard.KEY_RMENU -> {}
|
||||
default -> comboUsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean onF3Release() {
|
||||
final boolean toggle = armed && !comboUsed;
|
||||
disarm();
|
||||
return toggle;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,70 @@
|
||||
package com.gtnewhorizons.angelica.debug;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.GTNHLib;
|
||||
import com.gtnewhorizon.gtnhlib.config.ConfigurationManager;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasDebugScreenHandler;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugEntryDef;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugOptionsScreen;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreen;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreenEntries;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import cpw.mods.fml.common.gameevent.InputEvent;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.entity.RenderManager;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import org.lwjgl.input.Keyboard;
|
||||
|
||||
/** F3+V toggles {@link AngelicaConfig#verboseF3} */
|
||||
public class DebugKeyHandler {
|
||||
|
||||
public static final DebugF3Toggle F3_TOGGLE = new DebugF3Toggle();
|
||||
|
||||
@SubscribeEvent
|
||||
public void onKey(InputEvent.KeyInputEvent event) {
|
||||
if (!Keyboard.getEventKeyState() || Keyboard.getEventKey() != Keyboard.KEY_V || !Keyboard.isKeyDown(Keyboard.KEY_F3)
|
||||
|| Minecraft.getMinecraft().currentScreen != null) {
|
||||
final int key = Keyboard.getEventKey();
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
|
||||
if (mc.currentScreen != null) {
|
||||
if (mc.currentScreen instanceof DebugOptionsScreen && key == Keyboard.KEY_F6 && Keyboard.getEventKeyState()
|
||||
&& !Keyboard.isRepeatEvent() && Keyboard.isKeyDown(Keyboard.KEY_F3)) {
|
||||
mc.displayGuiScreen(null);
|
||||
}
|
||||
F3_TOGGLE.disarm();
|
||||
return;
|
||||
}
|
||||
|
||||
AngelicaConfig.verboseF3 = !AngelicaConfig.verboseF3;
|
||||
ConfigurationManager.save(AngelicaConfig.class);
|
||||
CeleritasDebugScreenHandler.invalidate();
|
||||
GTNHLib.proxy.addDebugToChat(StatCollector.translateToLocal(AngelicaConfig.verboseF3 ? "angelica.debug.verbose.on" : "angelica.debug.verbose.off"));
|
||||
if (!Keyboard.getEventKeyState()) {
|
||||
if (key == Keyboard.KEY_F3 && F3_TOGGLE.onF3Release()) {
|
||||
mc.gameSettings.showDebugInfo = !mc.gameSettings.showDebugInfo;
|
||||
mc.gameSettings.showDebugProfilerChart = F3_TOGGLE.shift();
|
||||
((IGameSettingsExt) mc.gameSettings).angelica$setShowFpsGraph(F3_TOGGLE.alt());
|
||||
}
|
||||
if (key == Keyboard.KEY_F3) DebugScreen.syncOverlayVisible();
|
||||
return;
|
||||
}
|
||||
|
||||
F3_TOGGLE.onKeyPress(key);
|
||||
|
||||
if (!Keyboard.isKeyDown(Keyboard.KEY_F3)) {
|
||||
return;
|
||||
}
|
||||
if (!Keyboard.isRepeatEvent()) {
|
||||
switch (key) {
|
||||
case Keyboard.KEY_F6 -> DebugOptionsScreen.open(mc);
|
||||
case Keyboard.KEY_V -> toggleAndReport(DebugScreenEntries.ANGELICA_RENDERER_VERBOSE, "angelica.debug.verbose");
|
||||
case Keyboard.KEY_G -> toggleAndReport(DebugScreenEntries.CHUNK_BORDERS, "angelica.debug.chunk_borders");
|
||||
case Keyboard.KEY_B -> {
|
||||
if (DebugScreen.ENTRIES != null) DebugScreen.ENTRIES.toggleStatus(DebugScreenEntries.ENTITY_HITBOXES);
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
if (key == Keyboard.KEY_B && DebugScreen.ENTRIES != null) {
|
||||
RenderManager.debugBoundingBox = DebugScreenEntries.ENTITY_HITBOXES.isEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
private static void toggleAndReport(DebugEntryDef def, String key) {
|
||||
if (DebugScreen.ENTRIES == null) return;
|
||||
final boolean on = DebugScreen.ENTRIES.toggleStatus(def);
|
||||
GTNHLib.proxy.addDebugToChat(StatCollector.translateToLocal(key + (on ? ".on" : ".off")));
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,44 @@
|
||||
package com.gtnewhorizons.angelica.debug;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugDrawScope;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugGeometry;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugPrograms;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugProgram;
|
||||
import com.gtnewhorizons.angelica.debug.render.NativeScratch;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.states.ViewportState;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.LineQuads;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.ScaledResolution;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraftforge.client.event.RenderGameOverlayEvent;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
public class F3Direction {
|
||||
private static final float AXIS_LENGTH = 10.0F;
|
||||
private static final float LINE_WIDTH = 2.0F;
|
||||
private static final int SEGMENTS = 3;
|
||||
private static final int LINE_VERTICES = SEGMENTS * 2;
|
||||
private static final int QUAD_VERTICES = SEGMENTS * 6;
|
||||
private static final int FLAGS = VertexFlags.COLOR_BIT;
|
||||
private static final VertexFormat FORMAT = DefaultVertexFormat.ALL_FORMATS[FLAGS];
|
||||
private static final VertexFormat QUAD_FORMAT = LineQuads.quadFormat(FORMAT);
|
||||
private static final int STRIDE = FORMAT.getVertexSize();
|
||||
private static final int COLOR_OFFSET = DebugGeometry.offsetOf(FORMAT, VertexFormatElement.Usage.COLOR);
|
||||
|
||||
private static final DebugDrawScope SCOPE = new DebugDrawScope();
|
||||
private static final Matrix4f MVP = new Matrix4f();
|
||||
private static int vao;
|
||||
|
||||
public static void renderWorldDirectionsEvent(Minecraft mc, RenderGameOverlayEvent.Pre event) {
|
||||
if (mc.gameSettings.showDebugInfo && mc.gameSettings.thirdPersonView == 0) {
|
||||
final ScaledResolution scaledresolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||
final int width = scaledresolution.getScaledWidth();
|
||||
final int height = scaledresolution.getScaledHeight();
|
||||
if (mc.gameSettings.thirdPersonView == 0) {
|
||||
final int width = event.resolution.getScaledWidth();
|
||||
final int height = event.resolution.getScaledHeight();
|
||||
|
||||
GLStateManager.glPushMatrix();
|
||||
|
||||
@@ -30,33 +55,57 @@ public class F3Direction {
|
||||
GLStateManager.glPopMatrix();
|
||||
}
|
||||
}
|
||||
|
||||
public static void renderWorldDirections() {
|
||||
GLStateManager.disableTexture();
|
||||
GLStateManager.glDepthMask(false);
|
||||
final Tessellator tessellator = Tessellator.instance;
|
||||
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), MVP);
|
||||
final ViewportState viewport = GLStateManager.getViewportState();
|
||||
|
||||
GLStateManager.glLineWidth(2.0F);
|
||||
tessellator.startDrawing(GL11.GL_LINES);
|
||||
SCOPE.beginHud();
|
||||
try {
|
||||
if (vao == 0) build();
|
||||
final DebugProgram lines = DebugPrograms.lines();
|
||||
lines.use();
|
||||
lines.mvp(MVP);
|
||||
lines.lineWidth(LINE_WIDTH);
|
||||
lines.viewport(viewport.width, viewport.height);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, QUAD_VERTICES);
|
||||
} finally {
|
||||
SCOPE.end();
|
||||
}
|
||||
}
|
||||
|
||||
//X
|
||||
tessellator.setColorRGBA_F(255, 0, 0, 1);
|
||||
tessellator.addVertex(0.0D, 0.0D, 0.0D);
|
||||
tessellator.addVertex(10, 0.0D, 0.0D);
|
||||
private static void build() {
|
||||
final int quadBytes = QUAD_VERTICES * QUAD_FORMAT.getVertexSize();
|
||||
final NativeScratch source = new NativeScratch(LINE_VERTICES * STRIDE);
|
||||
final NativeScratch expanded = new NativeScratch(quadBytes);
|
||||
try {
|
||||
source.ensure(LINE_VERTICES * STRIDE);
|
||||
expanded.ensure(quadBytes);
|
||||
long p = source.address();
|
||||
p = putAxis(p, AXIS_LENGTH, 0.0F, 0.0F, 0xFFFF0000);
|
||||
p = putAxis(p, 0.0F, 0.0F, AXIS_LENGTH, 0xFF0000FF);
|
||||
putAxis(p, 0.0F, AXIS_LENGTH, 0.0F, 0xFF00FF00);
|
||||
LineQuads.expandWithOtherEnd(source.address(), FORMAT, GL11.GL_LINES, LINE_VERTICES, expanded.address());
|
||||
|
||||
//Z
|
||||
tessellator.setColorRGBA_F(0, 0, 255, 1);
|
||||
tessellator.addVertex(0.0D, 0.0D, 0.0D);
|
||||
tessellator.addVertex(0.0D, 0.0D, 10);
|
||||
final int vbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, expanded.window(quadBytes), GL15.GL_STATIC_DRAW);
|
||||
vao = DebugGeometry.createVao(vbo, QUAD_FORMAT);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
} finally {
|
||||
source.free();
|
||||
expanded.free();
|
||||
}
|
||||
}
|
||||
|
||||
//Y
|
||||
tessellator.setColorRGBA_F(0, 255, 0, 1);
|
||||
tessellator.addVertex(0.0D, 0.0D, 0.0D);
|
||||
tessellator.addVertex(0.0D, 10, 0.0D);
|
||||
|
||||
tessellator.draw();
|
||||
|
||||
GLStateManager.glLineWidth(1.0F);
|
||||
GLStateManager.glDepthMask(true);
|
||||
GLStateManager.enableTexture();
|
||||
private static long putAxis(long address, float x, float y, float z, int argb) {
|
||||
long p = address;
|
||||
DebugGeometry.putPosition(p, 0.0F, 0.0F, 0.0F);
|
||||
DebugGeometry.putColor(p + COLOR_OFFSET, argb);
|
||||
p += STRIDE;
|
||||
DebugGeometry.putPosition(p, x, y, z);
|
||||
DebugGeometry.putColor(p + COLOR_OFFSET, argb);
|
||||
return p + STRIDE;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,31 @@
|
||||
package com.gtnewhorizons.angelica.debug;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.shader.ShaderProgram;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugDrawScope;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugGeometry;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugPrograms;
|
||||
import com.gtnewhorizons.angelica.debug.render.NativeScratch;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugNumbers;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.ScaledResolution;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.ShaderConstants;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack.stackPush;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
|
||||
import static org.lwjgl.opengl.GL11.GL_FLOAT;
|
||||
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
|
||||
import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
|
||||
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_STRIP;
|
||||
import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
|
||||
import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;
|
||||
@@ -29,14 +39,25 @@ public abstract class F3Graph {
|
||||
private static final int HEIGHT = 60;
|
||||
private static final int WIDTH = NUM_SAMPLES;
|
||||
private static final int FONT_COLOR = 0xFFE0E0E0;
|
||||
private static final int BACKGROUND_ARGB = 0x90505050;
|
||||
private static final int FOREGROUND_ARGB = 0xFFFFFFFF;
|
||||
private static final int COLOR_FLAGS = VertexFlags.COLOR_BIT;
|
||||
private static final int TEXTURED_FLAGS = VertexFlags.TEXTURE_BIT | VertexFlags.COLOR_BIT;
|
||||
private static final VertexFormat COLOR_FORMAT = DefaultVertexFormat.ALL_FORMATS[COLOR_FLAGS];
|
||||
private static final VertexFormat TEXTURED_FORMAT = DefaultVertexFormat.ALL_FORMATS[TEXTURED_FLAGS];
|
||||
private static final int COLOR_STRIDE = COLOR_FORMAT.getVertexSize();
|
||||
private static final int TEXTURED_STRIDE = TEXTURED_FORMAT.getVertexSize();
|
||||
private static final int TEXTURED_UV_OFFSET = DebugGeometry.offsetOf(TEXTURED_FORMAT, VertexFormatElement.Usage.PRIMARY_UV);
|
||||
private static final int TEXTURED_COLOR_OFFSET = DebugGeometry.offsetOf(TEXTURED_FORMAT, VertexFormatElement.Usage.COLOR);
|
||||
private static final int COLOR_COLOR_OFFSET = DebugGeometry.offsetOf(COLOR_FORMAT, VertexFormatElement.Usage.COLOR);
|
||||
private static final int QUAD_BYTES = 4 * Math.max(COLOR_STRIDE, TEXTURED_STRIDE);
|
||||
// Due to GLSL 120 limitations, it's just easier to use floats
|
||||
private final FloatBuffer sampleBuf = BufferUtils.createFloatBuffer(NUM_SAMPLES);
|
||||
private final long[] samples = new long[NUM_SAMPLES]; // long version for calculations
|
||||
// Circular buffer holding the last 240 samples, in nanoseconds
|
||||
private int samplesHead = 0; // one ahead of the position of the last sample
|
||||
private boolean initialized = false;
|
||||
private ShaderProgram shader;
|
||||
private int aPos;
|
||||
private int program;
|
||||
private int uFBWidth;
|
||||
private int uFBHeight;
|
||||
private int uScaleFactor;
|
||||
@@ -44,10 +65,20 @@ public abstract class F3Graph {
|
||||
private int uSamples;
|
||||
private int vertBuf;
|
||||
private int vao;
|
||||
private final DebugDrawScope scope = new DebugDrawScope();
|
||||
private final NativeScratch scratch = new NativeScratch(QUAD_BYTES);
|
||||
private final Matrix4f mvp = new Matrix4f();
|
||||
private final ResourceLocation texture;
|
||||
private final float pxPerNs;
|
||||
private final boolean left;
|
||||
private long sum;
|
||||
private final StringBuilder label = new StringBuilder(16);
|
||||
private String minString;
|
||||
private String avgString;
|
||||
private String maxString;
|
||||
private int labelTick = Integer.MIN_VALUE;
|
||||
private int avgWidth;
|
||||
private int maxWidth;
|
||||
|
||||
protected F3Graph(ResourceLocation texture, float pxPerNs, boolean left) {
|
||||
this.texture = texture;
|
||||
@@ -65,6 +96,10 @@ public abstract class F3Graph {
|
||||
samplesHead = (samplesHead + 1) % NUM_SAMPLES;
|
||||
}
|
||||
|
||||
public static int reservedHeight(FontRenderer fontRenderer) {
|
||||
return HEIGHT + BORDER * 2 + fontRenderer.FONT_HEIGHT;
|
||||
}
|
||||
|
||||
private int getVertX(ScaledResolution sres, int idx) {
|
||||
if (left) {
|
||||
return switch (idx) {
|
||||
@@ -92,23 +127,17 @@ public abstract class F3Graph {
|
||||
}
|
||||
|
||||
private void init() {
|
||||
shader = new ShaderProgram(
|
||||
"angelica",
|
||||
"shaders/debug_graph.vert.glsl",
|
||||
"shaders/debug_graph.frag.glsl");
|
||||
shader.use();
|
||||
final int handle = DebugPrograms.link("angelica:debug_graph", "angelica:debug_graph.vert.glsl", "angelica:debug_graph.frag.glsl", ShaderConstants.EMPTY, new String[]{"pos"}, new int[]{0});
|
||||
program = handle;
|
||||
GLStateManager.glUseProgram(handle);
|
||||
|
||||
// Attribute location is declared in shader via layout(location = 0)
|
||||
aPos = 0;
|
||||
|
||||
// Register uniforms
|
||||
uFBWidth = shader.getUniformLocation("fbWidth");
|
||||
uFBHeight = shader.getUniformLocation("fbHeight");
|
||||
uScaleFactor = shader.getUniformLocation("scaleFactor");
|
||||
uSamples = shader.getUniformLocation("samples");
|
||||
uHeadIdx = shader.getUniformLocation("headIdx");
|
||||
final int uPxPerNs = shader.getUniformLocation("pxPerNs");
|
||||
final int uLeft = shader.getUniformLocation("left");
|
||||
uFBWidth = GLStateManager.glGetUniformLocation(handle, "fbWidth");
|
||||
uFBHeight = GLStateManager.glGetUniformLocation(handle, "fbHeight");
|
||||
uScaleFactor = GLStateManager.glGetUniformLocation(handle, "scaleFactor");
|
||||
uSamples = GLStateManager.glGetUniformLocation(handle, "samples");
|
||||
uHeadIdx = GLStateManager.glGetUniformLocation(handle, "headIdx");
|
||||
final int uPxPerNs = GLStateManager.glGetUniformLocation(handle, "pxPerNs");
|
||||
final int uLeft = GLStateManager.glGetUniformLocation(handle, "left");
|
||||
|
||||
vao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
@@ -131,33 +160,17 @@ public abstract class F3Graph {
|
||||
}
|
||||
|
||||
// Configure vertex attribute in the VAO
|
||||
GLStateManager.glVertexAttribPointer(aPos, VERT_FLOATS, GL_FLOAT, false, VERT_FLOATS * 4, 0);
|
||||
GLStateManager.glEnableVertexAttribArray(aPos);
|
||||
GLStateManager.glVertexAttribPointer(0, VERT_FLOATS, GL_FLOAT, false, VERT_FLOATS * 4, 0);
|
||||
GLStateManager.glEnableVertexAttribArray(0);
|
||||
|
||||
GLStateManager.glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
final ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||
|
||||
// Load initial value for uniforms
|
||||
GLStateManager.glUniform1f(uFBWidth, mc.displayWidth);
|
||||
GLStateManager.glUniform1f(uFBHeight, mc.displayHeight);
|
||||
GLStateManager.glUniform1f(uScaleFactor, sr.getScaleFactor());
|
||||
GLStateManager.glUniform1i(uHeadIdx, samplesHead);
|
||||
GLStateManager.glUniform1(uSamples, sampleBuf);
|
||||
GLStateManager.glUniform1f(uPxPerNs, pxPerNs);
|
||||
GLStateManager.glUniform1i(uLeft, left ? 1 : 0); // this is how you load bool uniforms
|
||||
|
||||
ShaderProgram.clear();
|
||||
GLStateManager.glUniform1i(uLeft, left ? 1 : 0);
|
||||
}
|
||||
|
||||
public void render() {
|
||||
if (!initialized) {
|
||||
init();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public void render(ScaledResolution sr, int tick) {
|
||||
/*
|
||||
* We try to copy modern vanilla's tracker.
|
||||
* It is 240 (FRAME_TIMES) wide by 60 tall, including the 1px borders.
|
||||
@@ -171,88 +184,98 @@ public abstract class F3Graph {
|
||||
*/
|
||||
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
final ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||
|
||||
// Setup GL state
|
||||
final Tessellator tess = Tessellator.instance;
|
||||
GLStateManager.disableDepthTest();
|
||||
GLStateManager.enableAlphaTest();
|
||||
GLStateManager.enableBlend();
|
||||
GLStateManager.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), mvp);
|
||||
|
||||
// Draw background
|
||||
GLStateManager.disableTexture();
|
||||
GLStateManager.glColor4f(0x50 / 255.0F, 0x50 / 255.0F, 0x50 / 255.0F, 0x90 / 255.0F);
|
||||
tess.startDrawingQuads();
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
tess.addVertex(getVertX(sr, i), getVertY(sr, i), 0);
|
||||
scope.beginHud();
|
||||
try {
|
||||
if (!initialized) {
|
||||
init();
|
||||
initialized = true;
|
||||
}
|
||||
drawGraph(mc, sr);
|
||||
} finally {
|
||||
scope.end();
|
||||
}
|
||||
tess.draw();
|
||||
GLStateManager.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
GLStateManager.enableTexture();
|
||||
|
||||
|
||||
// Draw samples
|
||||
shader.use();
|
||||
// Draw text
|
||||
final FontRenderer fr = mc.fontRenderer;
|
||||
if (tick != labelTick) {
|
||||
labelTick = tick;
|
||||
updateLabels(fr);
|
||||
}
|
||||
final int top = sr.getScaledHeight() - reservedHeight(fr);
|
||||
final int scaledWidth = sr.getScaledWidth();
|
||||
final int minX;
|
||||
final int avgX;
|
||||
final int maxX;
|
||||
if (left) {
|
||||
minX = BORDER * 2;
|
||||
avgX = BORDER + WIDTH / 2 - avgWidth / 2;
|
||||
maxX = BORDER * 2 + WIDTH - maxWidth;
|
||||
} else {
|
||||
minX = scaledWidth - (BORDER * 2 + WIDTH);
|
||||
avgX = scaledWidth - (BORDER + WIDTH / 2 + avgWidth / 2);
|
||||
maxX = scaledWidth - (BORDER * 2 + maxWidth);
|
||||
}
|
||||
fr.drawString(minString, minX, top, FONT_COLOR, true);
|
||||
fr.drawString(avgString, avgX, top, FONT_COLOR, true);
|
||||
fr.drawString(maxString, maxX, top, FONT_COLOR, true);
|
||||
}
|
||||
|
||||
// Load uniforms
|
||||
private void drawGraph(Minecraft mc, ScaledResolution sr) {
|
||||
scratch.ensure(QUAD_BYTES);
|
||||
long p = scratch.address();
|
||||
for (int i = 0; i < 4; ++i, p += COLOR_STRIDE) {
|
||||
DebugGeometry.putPosition(p, getVertX(sr, i), getVertY(sr, i), 0.0F);
|
||||
DebugGeometry.putColor(p + COLOR_COLOR_OFFSET, BACKGROUND_ARGB);
|
||||
}
|
||||
final var color = DebugPrograms.color();
|
||||
color.use();
|
||||
color.mvp(mvp);
|
||||
TessellatorStreamingDrawer.drawPacked(scratch.window(4 * COLOR_STRIDE), GL11.GL_QUADS, COLOR_FLAGS, 4);
|
||||
|
||||
GLStateManager.glUseProgram(program);
|
||||
GLStateManager.glUniform1f(uFBWidth, mc.displayWidth);
|
||||
GLStateManager.glUniform1f(uFBHeight, mc.displayHeight);
|
||||
GLStateManager.glUniform1f(uScaleFactor, sr.getScaleFactor());
|
||||
GLStateManager.glUniform1i(uHeadIdx, samplesHead);
|
||||
GLStateManager.glUniform1(uSamples, sampleBuf);
|
||||
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
|
||||
// Left side graphs are inverted, so temporarily disable culling
|
||||
if (!left) GLStateManager.disableCull();
|
||||
|
||||
GLStateManager.glDrawArrays(GL_TRIANGLE_STRIP, 0, VERT_COUNT);
|
||||
|
||||
if (!left) GLStateManager.enableCull();
|
||||
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
ShaderProgram.clear();
|
||||
|
||||
// Draw foreground
|
||||
|
||||
mc.getTextureManager().bindTexture(texture);
|
||||
tess.startDrawingQuads();
|
||||
tess.addVertexWithUV(getVertX(sr, 0), getVertY(sr, 0), 0.0D, 1, 0);
|
||||
tess.addVertexWithUV(getVertX(sr, 1), getVertY(sr, 1), 0.0D, 0, 0);
|
||||
tess.addVertexWithUV(getVertX(sr, 2), getVertY(sr, 2), 0.0D, 0, 1);
|
||||
tess.addVertexWithUV(getVertX(sr, 3), getVertY(sr, 3), 0.0D, 1, 1);
|
||||
tess.draw();
|
||||
p = scratch.address();
|
||||
for (int i = 0; i < 4; ++i, p += TEXTURED_STRIDE) {
|
||||
DebugGeometry.putPosition(p, getVertX(sr, i), getVertY(sr, i), 0.0F);
|
||||
memPutFloat(p + TEXTURED_UV_OFFSET, (i == 0 || i == 3) ? 1.0F : 0.0F);
|
||||
memPutFloat(p + TEXTURED_UV_OFFSET + 4, i >= 2 ? 1.0F : 0.0F);
|
||||
DebugGeometry.putColor(p + TEXTURED_COLOR_OFFSET, FOREGROUND_ARGB);
|
||||
}
|
||||
final var textured = DebugPrograms.textured();
|
||||
textured.use();
|
||||
textured.mvp(mvp);
|
||||
TessellatorStreamingDrawer.drawPacked(scratch.window(4 * TEXTURED_STRIDE), GL11.GL_QUADS, TEXTURED_FLAGS, 4);
|
||||
}
|
||||
|
||||
// Reset GL state
|
||||
GLStateManager.enableDepthTest();
|
||||
GLStateManager.disableAlphaTest();
|
||||
GLStateManager.disableBlend();
|
||||
|
||||
// Draw text
|
||||
// Ensure running counters are reset on first sample
|
||||
private void updateLabels(FontRenderer fr) {
|
||||
long min = Long.MAX_VALUE;
|
||||
long max = Long.MIN_VALUE;
|
||||
for (int i = 0; i < NUM_SAMPLES; ++i) {
|
||||
min = Math.min(min, samples[i]);
|
||||
max = Math.max(max, samples[i]);
|
||||
}
|
||||
String minStr = String.format("%.1f ms min", min / 1_000_000D);
|
||||
String avgStr = String.format("%.1f ms avg", sum / 1_000_000 / (double) NUM_SAMPLES);
|
||||
String maxStr = String.format("%.1f ms max", max / 1_000_000D);
|
||||
final FontRenderer fr = mc.fontRenderer;
|
||||
final int top = sr.getScaledHeight() - HEIGHT - BORDER * 2 - fr.FONT_HEIGHT;
|
||||
final int scaledWidth = sr.getScaledWidth();
|
||||
minString = label(min / 1_000_000D, " ms min");
|
||||
avgString = label(sum / 1_000_000 / (double) NUM_SAMPLES, " ms avg");
|
||||
maxString = label(max / 1_000_000D, " ms max");
|
||||
avgWidth = fr.getStringWidth(avgString);
|
||||
maxWidth = fr.getStringWidth(maxString);
|
||||
}
|
||||
|
||||
if (left) {
|
||||
fr.drawString(minStr, BORDER * 2, top, FONT_COLOR, true);
|
||||
fr.drawString(avgStr, BORDER + WIDTH / 2 - fr.getStringWidth(avgStr) / 2, top, FONT_COLOR, true);
|
||||
fr.drawString(maxStr, BORDER * 2 + WIDTH - fr.getStringWidth(maxStr), top, FONT_COLOR, true);
|
||||
} else {
|
||||
fr.drawString(minStr, scaledWidth - (BORDER * 2 + WIDTH), top, FONT_COLOR, true);
|
||||
fr.drawString(avgStr, scaledWidth - (BORDER + WIDTH / 2 + fr.getStringWidth(avgStr) / 2), top, FONT_COLOR, true);
|
||||
fr.drawString(maxStr, scaledWidth - (BORDER * 2 + fr.getStringWidth(maxStr)), top, FONT_COLOR, true);
|
||||
}
|
||||
private String label(double millis, String suffix) {
|
||||
label.setLength(0);
|
||||
DebugNumbers.appendFixed(label, millis, 1);
|
||||
return label.append(suffix).toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.gtnewhorizons.angelica.debug.flyby;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugEntryDef;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugRegistry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreen;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreenEntryList;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreenProfile;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.gtnewhorizons.angelica.config.SystemProperties.FLYBY_DEBUG_ENTRIES;
|
||||
import static com.gtnewhorizons.angelica.config.SystemProperties.FLYBY_DEBUG_FLAT;
|
||||
import static com.gtnewhorizons.angelica.config.SystemProperties.FLYBY_DEBUG_HUD;
|
||||
import static com.gtnewhorizons.angelica.config.SystemProperties.FLYBY_DEBUG_PROFILE;
|
||||
import static com.gtnewhorizons.angelica.config.SystemProperties.FLYBY_FPS_GRAPH;
|
||||
import static com.gtnewhorizons.angelica.config.SystemProperties.FLYBY_HUD_CACHING;
|
||||
|
||||
final class FlybyDebugOverrides {
|
||||
private static final Logger LOGGER = LogManager.getLogger("Angelica/Flyby");
|
||||
|
||||
record Pair(String id, DebugScreenEntryStatus status) {}
|
||||
|
||||
private Boolean hudSaved;
|
||||
private Boolean flatSaved;
|
||||
private Boolean cachingSaved;
|
||||
private Boolean fpsGraphSaved;
|
||||
private boolean sessionOverridden;
|
||||
|
||||
static List<Pair> parseEntries(String raw) {
|
||||
final List<Pair> pairs = new ArrayList<>();
|
||||
if (raw == null) return pairs;
|
||||
for (final String part : raw.split(",")) {
|
||||
final String pair = part.trim();
|
||||
if (pair.isEmpty()) continue;
|
||||
final int eq = pair.lastIndexOf('=');
|
||||
if (eq <= 0 || eq == pair.length() - 1) {
|
||||
LOGGER.error("Flyby debugEntries: malformed pair '{}', expected id=status", pair);
|
||||
continue;
|
||||
}
|
||||
final String id = pair.substring(0, eq).trim();
|
||||
final String name = pair.substring(eq + 1).trim();
|
||||
final DebugScreenEntryStatus status = DebugScreenEntryStatus.bySerializedName(name);
|
||||
if (id.isEmpty() || status == null) {
|
||||
LOGGER.error("Flyby debugEntries: bad {} '{}' in '{}'", id.isEmpty() ? "id" : "status", id.isEmpty() ? id : name, pair);
|
||||
continue;
|
||||
}
|
||||
pairs.add(new Pair(id, status));
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
void apply(Minecraft mc) {
|
||||
if (FLYBY_DEBUG_HUD != null && this.hudSaved == null) {
|
||||
this.hudSaved = mc.gameSettings.showDebugInfo;
|
||||
mc.gameSettings.showDebugInfo = FLYBY_DEBUG_HUD;
|
||||
}
|
||||
if (FLYBY_DEBUG_FLAT != null && this.flatSaved == null) {
|
||||
this.flatSaved = AngelicaConfig.modernizeF3Screen;
|
||||
AngelicaConfig.modernizeF3Screen = !FLYBY_DEBUG_FLAT;
|
||||
}
|
||||
if (FLYBY_HUD_CACHING != null && this.cachingSaved == null) {
|
||||
this.cachingSaved = AngelicaConfig.hudCachingActive;
|
||||
AngelicaConfig.hudCachingActive = FLYBY_HUD_CACHING;
|
||||
}
|
||||
if (FLYBY_FPS_GRAPH != null && this.fpsGraphSaved == null) {
|
||||
final IGameSettingsExt settings = (IGameSettingsExt) mc.gameSettings;
|
||||
this.fpsGraphSaved = settings.angelica$showFpsGraph();
|
||||
settings.angelica$setShowFpsGraph(FLYBY_FPS_GRAPH);
|
||||
}
|
||||
this.applySession();
|
||||
}
|
||||
|
||||
private void applySession() {
|
||||
final boolean wantProfile = !FLYBY_DEBUG_PROFILE.isEmpty();
|
||||
final boolean wantEntries = !FLYBY_DEBUG_ENTRIES.isEmpty();
|
||||
if (!wantProfile && !wantEntries) return;
|
||||
|
||||
final DebugScreenEntryList entries = DebugScreen.ENTRIES;
|
||||
if (entries == null) {
|
||||
LOGGER.error("Flyby: debug entry list is not available, ignoring debugProfile and debugEntries");
|
||||
return;
|
||||
}
|
||||
final DebugRegistry registry = DebugRegistry.MAIN;
|
||||
this.sessionOverridden = true;
|
||||
|
||||
if (wantProfile) {
|
||||
final DebugScreenProfile profile = DebugScreenProfile.bySerializedName(FLYBY_DEBUG_PROFILE);
|
||||
if (profile == null) {
|
||||
LOGGER.error("Flyby: unknown debugProfile '{}', expected default or performance", FLYBY_DEBUG_PROFILE);
|
||||
} else {
|
||||
for (int i = 0; i < registry.entryCount(); i++) {
|
||||
final DebugEntryDef def = registry.entryAt(i);
|
||||
entries.overrideForSession(def, def.profileStatus(profile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final Pair pair : parseEntries(FLYBY_DEBUG_ENTRIES)) {
|
||||
final DebugEntryDef def = registry.entryById(pair.id());
|
||||
if (def == null) {
|
||||
LOGGER.error("Flyby debugEntries: unknown entry id '{}'", pair.id());
|
||||
continue;
|
||||
}
|
||||
entries.overrideForSession(def, pair.status());
|
||||
}
|
||||
}
|
||||
|
||||
void restore(Minecraft mc) {
|
||||
if (this.hudSaved != null) {
|
||||
mc.gameSettings.showDebugInfo = this.hudSaved;
|
||||
this.hudSaved = null;
|
||||
}
|
||||
if (this.flatSaved != null) {
|
||||
AngelicaConfig.modernizeF3Screen = this.flatSaved;
|
||||
this.flatSaved = null;
|
||||
}
|
||||
if (this.cachingSaved != null) {
|
||||
AngelicaConfig.hudCachingActive = this.cachingSaved;
|
||||
this.cachingSaved = null;
|
||||
}
|
||||
if (this.fpsGraphSaved != null) {
|
||||
((IGameSettingsExt) mc.gameSettings).angelica$setShowFpsGraph(this.fpsGraphSaved);
|
||||
this.fpsGraphSaved = null;
|
||||
}
|
||||
if (this.sessionOverridden) {
|
||||
final DebugScreenEntryList entries = DebugScreen.ENTRIES;
|
||||
if (entries != null) entries.clearSessionOverrides();
|
||||
this.sessionOverridden = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.config.SystemProperties.FlybyPacing;
|
||||
import com.gtnewhorizons.angelica.debug.profiling.AsprofRecorder;
|
||||
import com.gtnewhorizons.angelica.debug.profiling.AsprofRecorder.StopResult;
|
||||
import com.gtnewhorizons.angelica.debug.profiling.AsprofRecorder.StopStatus;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreenOverlay;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
@@ -14,6 +15,7 @@ import com.gtnewhorizons.angelica.rendering.FramePacer;
|
||||
import com.gtnewhorizons.angelica.rendering.IsbrhTestRenderer;
|
||||
import com.sun.management.OperatingSystemMXBean;
|
||||
import com.sun.management.ThreadMXBean;
|
||||
import cpw.mods.fml.common.FMLCommonHandler;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import cpw.mods.fml.common.gameevent.TickEvent;
|
||||
import net.minecraft.client.Minecraft;
|
||||
@@ -92,8 +94,7 @@ public final class FlybyRunner {
|
||||
private boolean pauseOnLostFocusSaved;
|
||||
private boolean pauseOnLostFocusOverridden;
|
||||
private boolean vsyncOverridden;
|
||||
private boolean showDebugInfoSaved;
|
||||
private boolean showDebugInfoOverridden;
|
||||
private final FlybyDebugOverrides debugOverrides = new FlybyDebugOverrides();
|
||||
private int cameraSaved;
|
||||
private boolean cameraOverridden;
|
||||
private int shotEvery;
|
||||
@@ -180,8 +181,8 @@ public final class FlybyRunner {
|
||||
this.fixedOrigin = origin;
|
||||
this.waitForTracy = SystemProperties.FLYBY_WAIT_FOR_TRACY;
|
||||
if (this.waitForTracy && !Tracy.ENABLED) {
|
||||
LOGGER.warn("Flyby: waitForTracy requested but Tracy is disabled in this build");
|
||||
this.waitForTracy = false;
|
||||
LOGGER.error("Flyby: a Tracy capture was requested but Tracy is not available (native library missing or init failed, see the Tracy log lines above); exiting instead of running unprofiled");
|
||||
FMLCommonHandler.instance().exitJava(1, false);
|
||||
}
|
||||
this.waitForFocus = SystemProperties.FLYBY_WAIT_FOR_FOCUS;
|
||||
this.exitWhenDone = SystemProperties.FLYBY_EXIT_WHEN_DONE;
|
||||
@@ -339,6 +340,7 @@ public final class FlybyRunner {
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
final IChatComponent result = ScreenShotHelper.saveScreenshot(mc.mcDataDir, "flyby-shot-" + this.shot + ".png", mc.displayWidth, mc.displayHeight, mc.getFramebuffer());
|
||||
LOGGER.info("Flyby screenshot {}", result.getUnformattedText());
|
||||
if (SystemProperties.FLYBY_DEBUG_DUMP) DebugScreenOverlay.INSTANCE.describe(LOGGER, this.shot);
|
||||
this.shot++;
|
||||
this.nextShot += this.shotEvery;
|
||||
}
|
||||
@@ -401,11 +403,7 @@ public final class FlybyRunner {
|
||||
|
||||
this.buildPath();
|
||||
this.suppressPacing();
|
||||
if (SystemProperties.FLYBY_DEBUG_HUD && !this.showDebugInfoOverridden) {
|
||||
this.showDebugInfoSaved = mc.gameSettings.showDebugInfo;
|
||||
this.showDebugInfoOverridden = true;
|
||||
mc.gameSettings.showDebugInfo = true;
|
||||
}
|
||||
this.debugOverrides.apply(mc);
|
||||
if (!this.cameraOverridden) {
|
||||
this.cameraSaved = mc.gameSettings.thirdPersonView;
|
||||
this.cameraOverridden = true;
|
||||
@@ -682,6 +680,7 @@ public final class FlybyRunner {
|
||||
ThreadedFileIOBase.threadedIOInstance.waitForFinish();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOGGER.error("Flyby: interrupted while draining chunk IO before discarding changes", e);
|
||||
}
|
||||
|
||||
worldChangesDiscarded = true;
|
||||
@@ -878,7 +877,7 @@ public final class FlybyRunner {
|
||||
|
||||
this.returnToOrigin(player);
|
||||
this.restorePauseOnLostFocus(mc);
|
||||
this.restoreShowDebugInfo(mc);
|
||||
this.debugOverrides.restore(mc);
|
||||
this.restoreCamera(mc);
|
||||
this.armed = false;
|
||||
sceneGuarded = false;
|
||||
@@ -906,12 +905,6 @@ public final class FlybyRunner {
|
||||
this.pauseOnLostFocusOverridden = false;
|
||||
}
|
||||
|
||||
private void restoreShowDebugInfo(Minecraft mc) {
|
||||
if (!this.showDebugInfoOverridden) return;
|
||||
mc.gameSettings.showDebugInfo = this.showDebugInfoSaved;
|
||||
this.showDebugInfoOverridden = false;
|
||||
}
|
||||
|
||||
private void restoreCamera(Minecraft mc) {
|
||||
if (!this.cameraOverridden) return;
|
||||
mc.gameSettings.thirdPersonView = this.cameraSaved;
|
||||
@@ -1067,7 +1060,7 @@ public final class FlybyRunner {
|
||||
FramePacer.endStats();
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
this.restorePauseOnLostFocus(mc);
|
||||
this.restoreShowDebugInfo(mc);
|
||||
this.debugOverrides.restore(mc);
|
||||
this.restoreCamera(mc);
|
||||
this.armed = false;
|
||||
this.stopRecording(mc);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.gtnewhorizons.angelica.debug.render;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.StateSet;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
public final class DebugDrawScope {
|
||||
|
||||
private static final StateSet SET = StateSet.forMask(GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_ENABLE_BIT | GL11.GL_POLYGON_BIT | GL11.GL_SCISSOR_BIT | GL11.GL_TEXTURE_BIT | GL11.GL_LINE_BIT);
|
||||
|
||||
private int depth;
|
||||
private int program;
|
||||
private int vao;
|
||||
private int vbo;
|
||||
|
||||
public void beginHud() {
|
||||
capture();
|
||||
GLStateManager.disableDepthTest();
|
||||
common();
|
||||
}
|
||||
|
||||
public void beginWorld() {
|
||||
capture();
|
||||
GLStateManager.enableDepthTest();
|
||||
GLStateManager.glDepthFunc(GL11.GL_LEQUAL);
|
||||
GLStateManager.glCullFace(GL11.GL_BACK);
|
||||
GLStateManager.glFrontFace(GL11.GL_CCW);
|
||||
common();
|
||||
}
|
||||
|
||||
private void capture() {
|
||||
depth = GLStateManager.pushState(SET);
|
||||
program = GLStateManager.getActiveProgram();
|
||||
vao = GLStateManager.getBoundVAO();
|
||||
vbo = GLStateManager.getBoundVBO();
|
||||
}
|
||||
|
||||
public void end() {
|
||||
GLStateManager.glUseProgram(program);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GLStateManager.popStateTo(depth);
|
||||
}
|
||||
|
||||
private static void common() {
|
||||
GLStateManager.glDepthMask(false);
|
||||
GLStateManager.enableBlend();
|
||||
GLStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO);
|
||||
GLStateManager.disableCull();
|
||||
GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL);
|
||||
GLStateManager.disableScissorTest();
|
||||
GLStateManager.glColorMask(true, true, true, true);
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
GLStateManager.glLineWidth(1.0F);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.gtnewhorizons.angelica.debug.render;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutByte;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
|
||||
|
||||
public final class DebugGeometry {
|
||||
|
||||
private DebugGeometry() {}
|
||||
|
||||
public static int createVao(int vbo, VertexFormat format) {
|
||||
final int vao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
format.setupBufferState(0L);
|
||||
return vao;
|
||||
}
|
||||
|
||||
public static int offsetOf(VertexFormat format, VertexFormatElement.Usage usage) {
|
||||
int offset = 0;
|
||||
for (final VertexFormatElement element : format.elementsArray) {
|
||||
if (element.getUsage() == usage) return offset;
|
||||
offset += element.getByteSize();
|
||||
}
|
||||
throw new IllegalArgumentException("Vertex format has no " + usage);
|
||||
}
|
||||
|
||||
public static void putColor(long address, int argb) {
|
||||
memPutByte(address, (byte) (argb >> 16));
|
||||
memPutByte(address + 1, (byte) (argb >> 8));
|
||||
memPutByte(address + 2, (byte) argb);
|
||||
memPutByte(address + 3, (byte) (argb >>> 24));
|
||||
}
|
||||
|
||||
public static void putPosition(long address, float x, float y, float z) {
|
||||
memPutFloat(address, x);
|
||||
memPutFloat(address + 4, y);
|
||||
memPutFloat(address + 8, z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.gtnewhorizons.angelica.debug.render;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.joml.Matrix4fc;
|
||||
import org.lwjgl.BufferUtils;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
public final class DebugProgram {
|
||||
|
||||
private final int handle;
|
||||
private final int mvpLocation;
|
||||
private final int lineWidthLocation;
|
||||
private final int viewportLocation;
|
||||
private final FloatBuffer matrix = BufferUtils.createFloatBuffer(16);
|
||||
|
||||
DebugProgram(int handle) {
|
||||
this.handle = handle;
|
||||
this.mvpLocation = GLStateManager.glGetUniformLocation(handle, "u_MVP");
|
||||
this.lineWidthLocation = GLStateManager.glGetUniformLocation(handle, "u_LineWidth");
|
||||
this.viewportLocation = GLStateManager.glGetUniformLocation(handle, "u_ViewportSize");
|
||||
}
|
||||
|
||||
public void use() {
|
||||
GLStateManager.glUseProgram(handle);
|
||||
}
|
||||
|
||||
public void mvp(Matrix4fc mvp) {
|
||||
mvp.get(matrix);
|
||||
GLStateManager.glUniformMatrix4(mvpLocation, false, matrix);
|
||||
}
|
||||
|
||||
public void lineWidth(float pixels) {
|
||||
GLStateManager.glUniform1f(lineWidthLocation, pixels);
|
||||
}
|
||||
|
||||
public void viewport(int width, int height) {
|
||||
GLStateManager.glUniform2f(viewportLocation, width, height);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.gtnewhorizons.angelica.debug.render;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.GlProgram;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.GlShader;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.ShaderConstants;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.ShaderType;
|
||||
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
|
||||
|
||||
public final class DebugPrograms {
|
||||
|
||||
private static DebugProgram color;
|
||||
private static DebugProgram textured;
|
||||
private static DebugProgram lines;
|
||||
|
||||
private DebugPrograms() {}
|
||||
|
||||
public static DebugProgram color() {
|
||||
if (color == null) color = link("angelica:debug_color", "angelica:debug.vert", ShaderConstants.EMPTY, false, false);
|
||||
return color;
|
||||
}
|
||||
|
||||
public static DebugProgram textured() {
|
||||
if (textured == null) textured = link("angelica:debug_textured", "angelica:debug.vert", ShaderConstants.builder().add("TEXTURED").build(), true, false);
|
||||
return textured;
|
||||
}
|
||||
|
||||
public static DebugProgram lines() {
|
||||
if (lines == null) lines = link("angelica:debug_lines", "angelica:debug_lines.vert", ShaderConstants.EMPTY, false, true);
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static DebugProgram link(String name, String vertexSource, ShaderConstants constants, boolean texCoord, boolean otherEnd) {
|
||||
final int count = 2 + (texCoord ? 1 : 0) + (otherEnd ? 1 : 0);
|
||||
final String[] attributes = new String[count];
|
||||
final int[] locations = new int[count];
|
||||
attributes[0] = "a_Position";
|
||||
locations[0] = VertexFormatElement.Usage.POSITION.getAttributeLocation();
|
||||
attributes[1] = "a_Color";
|
||||
locations[1] = VertexFormatElement.Usage.COLOR.getAttributeLocation();
|
||||
int next = 2;
|
||||
if (texCoord) {
|
||||
attributes[next] = "a_TexCoord0";
|
||||
locations[next++] = VertexFormatElement.Usage.PRIMARY_UV.getAttributeLocation();
|
||||
}
|
||||
if (otherEnd) {
|
||||
attributes[next] = "a_LineOtherEnd";
|
||||
locations[next] = VertexFormatElement.Usage.NORMAL.getAttributeLocation();
|
||||
}
|
||||
return new DebugProgram(link(name, vertexSource, "angelica:debug.frag", constants, attributes, locations));
|
||||
}
|
||||
|
||||
public static int link(String name, String vertexSource, String fragmentSource, ShaderConstants constants, String[] attributes, int[] locations) {
|
||||
final GlShader vert = ShaderLoader.loadShader(ShaderType.VERTEX, vertexSource, constants);
|
||||
final GlShader frag = ShaderLoader.loadShader(ShaderType.FRAGMENT, fragmentSource, constants);
|
||||
try {
|
||||
final GlProgram.Builder builder = GlProgram.builder(name).attachShader(vert).attachShader(frag);
|
||||
for (int i = 0; i < attributes.length; i++) builder.bindAttribute(attributes[i], locations[i]);
|
||||
final GlProgram<Void> program = builder.link(ctx -> null);
|
||||
return program.handle();
|
||||
} finally {
|
||||
vert.delete();
|
||||
frag.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.gtnewhorizons.angelica.debug.render;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memRealloc;
|
||||
|
||||
public final class NativeScratch {
|
||||
|
||||
private final int initial;
|
||||
private ByteBuffer buffer;
|
||||
private long address;
|
||||
private int capacity;
|
||||
|
||||
public NativeScratch(int initial) {
|
||||
this.initial = initial;
|
||||
}
|
||||
|
||||
public long address() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public void ensure(int bytes) {
|
||||
if (bytes <= capacity) return;
|
||||
int grown = Math.max(1, Math.max(initial, capacity));
|
||||
while (grown < bytes) grown <<= 1;
|
||||
buffer = memRealloc(buffer, grown);
|
||||
address = memAddress0(buffer);
|
||||
capacity = buffer.capacity();
|
||||
}
|
||||
|
||||
public ByteBuffer window(int bytes) {
|
||||
buffer.clear();
|
||||
buffer.limit(bytes);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public void free() {
|
||||
memFree(buffer);
|
||||
buffer = null;
|
||||
address = 0L;
|
||||
capacity = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.entity.EntityClientPlayerMP;
|
||||
import net.minecraft.util.MathHelper;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
|
||||
public final class DebugContextImpl implements DebugContext {
|
||||
|
||||
private static final long SAMPLE_INTERVAL_NS = 500_000_000L;
|
||||
|
||||
private World world;
|
||||
private EntityClientPlayerMP player;
|
||||
private Chunk chunk;
|
||||
private int blockX;
|
||||
private int blockY;
|
||||
private int blockZ;
|
||||
private long nanoTime;
|
||||
private boolean sampleDue;
|
||||
private boolean sampled;
|
||||
private long sampledVersion;
|
||||
private long sampledNanos;
|
||||
private DebugEntryDef entry;
|
||||
private boolean entryFirstSample;
|
||||
|
||||
public void prepare(Minecraft mc, long nanoTime, long entriesVersion) {
|
||||
final World w = mc.theWorld;
|
||||
final EntityClientPlayerMP p = mc.thePlayer;
|
||||
world = w;
|
||||
player = p;
|
||||
chunk = null;
|
||||
if (w != null && p != null) {
|
||||
blockX = MathHelper.floor_double(p.posX);
|
||||
blockY = MathHelper.floor_double(p.boundingBox.minY);
|
||||
blockZ = MathHelper.floor_double(p.posZ);
|
||||
final Chunk column = w.getChunkFromChunkCoords(blockX >> 4, blockZ >> 4);
|
||||
if (column != null && !column.isEmpty()) chunk = column;
|
||||
}
|
||||
this.nanoTime = nanoTime;
|
||||
sampleDue = !sampled || entriesVersion != sampledVersion || nanoTime - sampledNanos >= SAMPLE_INTERVAL_NS;
|
||||
if (sampleDue) {
|
||||
sampled = true;
|
||||
sampledVersion = entriesVersion;
|
||||
sampledNanos = nanoTime;
|
||||
}
|
||||
}
|
||||
|
||||
void beginEntry(DebugEntryDef def) {
|
||||
entry = def;
|
||||
entryFirstSample = false;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
world = null;
|
||||
player = null;
|
||||
chunk = null;
|
||||
entry = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public World world() {
|
||||
return world;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityClientPlayerMP player() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int blockX() {
|
||||
return blockX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int blockY() {
|
||||
return blockY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int blockZ() {
|
||||
return blockZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Chunk chunk() {
|
||||
return chunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nanoTime() {
|
||||
return nanoTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSampleDue() {
|
||||
final DebugEntryDef def = entry;
|
||||
if (def != null && !def.sampleOffered) {
|
||||
def.sampleOffered = true;
|
||||
entryFirstSample = true;
|
||||
}
|
||||
return sampleDue || entryFirstSample;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryCategory;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntrySpec;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntrySpec.DebugEntryFactory;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntrySpec.DebugToggleListener;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus;
|
||||
|
||||
public final class DebugEntryDef implements DebugEntryHandle {
|
||||
|
||||
public final String id;
|
||||
public final String namespace;
|
||||
public final String path;
|
||||
public final String name;
|
||||
public final DebugEntryCategory category;
|
||||
public final DebugScreenEntryStatus defaultStatus;
|
||||
public final DebugScreenEntryStatus performanceStatus;
|
||||
public final int index;
|
||||
public final int kind;
|
||||
public final int rendererIndex;
|
||||
public final boolean supportsAlways;
|
||||
public final boolean flat;
|
||||
|
||||
final DebugRegistry registry;
|
||||
final DebugEntryFactory textFactory;
|
||||
final DebugToggleListener toggleListener;
|
||||
|
||||
boolean enabled;
|
||||
boolean alwaysOn;
|
||||
boolean failed;
|
||||
boolean sampleOffered;
|
||||
private DebugScreenEntry entry;
|
||||
|
||||
DebugEntryDef(DebugRegistry registry, DebugEntrySpec spec, int index, int kind, int rendererIndex, boolean flat) {
|
||||
this.registry = registry;
|
||||
this.id = spec.getId();
|
||||
final int colon = id.indexOf(':');
|
||||
this.namespace = id.substring(0, colon);
|
||||
this.path = id.substring(colon + 1);
|
||||
this.name = spec.getName();
|
||||
this.category = spec.getCategory();
|
||||
this.defaultStatus = spec.getDefaultStatus();
|
||||
this.performanceStatus = spec.getPerformanceStatus();
|
||||
this.index = index;
|
||||
this.kind = kind;
|
||||
this.rendererIndex = rendererIndex;
|
||||
this.supportsAlways = spec.getSupportsAlways();
|
||||
this.flat = flat;
|
||||
this.textFactory = spec.getTextFactory();
|
||||
this.toggleListener = spec.getToggleListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public boolean isAlwaysOn() {
|
||||
return alwaysOn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFailed() {
|
||||
return failed;
|
||||
}
|
||||
|
||||
public boolean hasText() {
|
||||
return textFactory != null;
|
||||
}
|
||||
|
||||
public boolean isScreenText() {
|
||||
return category == DebugEntryCategory.SCREEN_TEXT && hasText();
|
||||
}
|
||||
|
||||
public DebugScreenEntryStatus profileStatus(DebugScreenProfile profile) {
|
||||
return profile == DebugScreenProfile.PERFORMANCE ? performanceStatus : defaultStatus;
|
||||
}
|
||||
|
||||
public DebugScreenEntry entryOrCreate() {
|
||||
DebugScreenEntry created = entry;
|
||||
if (created == null && textFactory != null) {
|
||||
created = textFactory.create(this);
|
||||
entry = created;
|
||||
}
|
||||
return created;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugFact;
|
||||
|
||||
public final class DebugFactImpl implements DebugFact {
|
||||
|
||||
private static final String VALUE_ON = "\u00a7f";
|
||||
private static final String VALUE_OFF = "\u00a7r";
|
||||
|
||||
private static final String PLAIN = "";
|
||||
|
||||
private StringBuilder out;
|
||||
private String valueOn = VALUE_ON;
|
||||
private String valueOff = VALUE_OFF;
|
||||
|
||||
public DebugFactImpl into(StringBuilder out) {
|
||||
this.out = out;
|
||||
valueOn = VALUE_ON;
|
||||
valueOff = VALUE_OFF;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugFactImpl intoPlain(StringBuilder out) {
|
||||
this.out = out;
|
||||
valueOn = PLAIN;
|
||||
valueOff = PLAIN;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact text(CharSequence text) {
|
||||
out.append(text);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact text(char c) {
|
||||
out.append(c);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact value(CharSequence value) {
|
||||
out.append(valueOn).append(value).append(valueOff);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact value(int value) {
|
||||
out.append(valueOn).append(value).append(valueOff);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact value(long value) {
|
||||
out.append(valueOn).append(value).append(valueOff);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact value(double value, int decimals) {
|
||||
out.append(valueOn);
|
||||
DebugNumbers.appendFixed(out, value, decimals);
|
||||
out.append(valueOff);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact padded(long value, int width) {
|
||||
out.append(valueOn);
|
||||
DebugNumbers.appendPadded(out, value, width);
|
||||
out.append(valueOff);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupSpec;
|
||||
|
||||
public final class DebugGroup implements DebugGroupHandle {
|
||||
|
||||
public static final String PRIORITY_LEFT_ID = "angelica:priority_left";
|
||||
|
||||
public static final DebugGroup PRIORITY_LEFT = builtin("priority_left", null, -1, LEFT, 0);
|
||||
public static final DebugGroup RENDERING = builtin("rendering", "rendering", 0xF0B0A0, LEFT, 100);
|
||||
public static final DebugGroup POSITION = builtin("position", "position", 0xFFFFFF, LEFT, 200);
|
||||
public static final DebugGroup SHADERS = builtin("shaders", "shaders", 0xFF55FF, LEFT, 300);
|
||||
public static final DebugGroup OTHER_LEFT = builtin("other_left", "other", -1, LEFT, 800);
|
||||
public static final DebugGroup HELP = builtin("help", "help", -1, LEFT, 1000);
|
||||
public static final DebugGroup MEMORY = builtin("memory", "memory", 0xFF9B00, RIGHT, 100);
|
||||
public static final DebugGroup SYSTEM_SPECS = builtin("system_specs", "system_specs", 0xFF0000, RIGHT, 200);
|
||||
public static final DebugGroup FORGE = builtin("forge", "forge", 0x5555FF, RIGHT, 300);
|
||||
public static final DebugGroup ANGELICA = builtin("angelica", "angelica", 0x55FF55, RIGHT, 400);
|
||||
public static final DebugGroup OTHER_RIGHT = builtin("other_right", "other", -1, RIGHT, 800);
|
||||
public static final DebugGroup LOOKING_AT_BLOCK = builtin("looking_at_block", "looking_at_block", 0xCC00FF, RIGHT, 900);
|
||||
|
||||
public final String id;
|
||||
public final String titleKey;
|
||||
public final int accentColor;
|
||||
public final int side;
|
||||
public final int order;
|
||||
int index = -1;
|
||||
|
||||
DebugGroup(String id, String titleKey, int accentColor, int side, int order) {
|
||||
this.id = id;
|
||||
this.titleKey = titleKey;
|
||||
this.accentColor = accentColor;
|
||||
this.side = side;
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
private static DebugGroup builtin(String path, String title, int accent, int side, int order) {
|
||||
final DebugGroupSpec spec = new DebugGroupSpec("angelica:" + path).side(side).order(order);
|
||||
if (title != null) spec.title("angelica.debug.group." + title);
|
||||
if (accent >= 0) spec.accent(accent);
|
||||
return DebugRegistry.MAIN.registerGroup(spec);
|
||||
}
|
||||
|
||||
static void bootstrap() {}
|
||||
|
||||
static DebugGroup of(DebugGroupHandle handle) {
|
||||
if (handle instanceof DebugGroup group) return group;
|
||||
throw new IllegalArgumentException("Not a debug group handle from DebugScreenApi: " + handle);
|
||||
}
|
||||
|
||||
static String requireName(String name) {
|
||||
if (name == null) throw new IllegalArgumentException("Debug fact name is null");
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int side() {
|
||||
return side;
|
||||
}
|
||||
|
||||
public int index() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public boolean hasTitle() {
|
||||
return titleKey != null;
|
||||
}
|
||||
|
||||
public boolean hasAccent() {
|
||||
return accentColor >= 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class DebugGroupContents {
|
||||
|
||||
public static final int TEXT = 1;
|
||||
public static final int SIZE = 2;
|
||||
|
||||
public static final int ROW_HEIGHT = 9;
|
||||
private static final int SHRINK_HOLD_TICKS = 40;
|
||||
public static final int MARGIN = 3;
|
||||
static final int TITLE_PADDING = 3;
|
||||
static final int FACT_PADDING = 5;
|
||||
static final int FACT_LINE_GAP = 2;
|
||||
static final int MIN_VALUE_BUDGET = 32;
|
||||
|
||||
static final int TITLE_BAR_COLOR = 0x90303030;
|
||||
static final int BODY_COLOR = 0x90505050;
|
||||
static final int TITLE_COLOR = 0xFFFFFFFF;
|
||||
static final int NAME_COLOR = 0xFFE0E0E0;
|
||||
static final int VALUE_COLOR = 0xFFD0D0D0;
|
||||
static final int LINE_COLOR = 0xFFE0E0E0;
|
||||
|
||||
private static final Slot[] NO_SLOTS = new Slot[0];
|
||||
|
||||
public static final class Slot {
|
||||
|
||||
StringBuilder text = new StringBuilder();
|
||||
private StringBuilder previous = new StringBuilder();
|
||||
final StringBuilder wrapped = new StringBuilder();
|
||||
int[] rowStart = new int[1];
|
||||
int[] rowEnd = new int[1];
|
||||
int rowCount;
|
||||
int width;
|
||||
String name;
|
||||
String label;
|
||||
int nameWidth;
|
||||
private int wrapBudget = -1;
|
||||
private boolean isWrapped;
|
||||
private boolean dirty;
|
||||
|
||||
void begin() {
|
||||
final StringBuilder swap = previous;
|
||||
previous = text;
|
||||
text = swap;
|
||||
text.setLength(0);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
private boolean sameAsPrevious() {
|
||||
return CharSequence.compare(text, previous) == 0;
|
||||
}
|
||||
|
||||
boolean measure(DebugText renderer, int budget, boolean force) {
|
||||
boolean textChanged = false;
|
||||
if (dirty) {
|
||||
dirty = false;
|
||||
textChanged = !sameAsPrevious();
|
||||
}
|
||||
if (!force && !textChanged && budget == wrapBudget) return false;
|
||||
wrapBudget = budget;
|
||||
rowCount = 0;
|
||||
width = 0;
|
||||
final int len = text.length();
|
||||
final float full = renderer.measure(text, 0, len);
|
||||
if (full <= budget) {
|
||||
isWrapped = false;
|
||||
addRow(0, len, full);
|
||||
} else {
|
||||
isWrapped = true;
|
||||
renderer.wrap(this, budget);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void addRow(int start, int end, float rowWidth) {
|
||||
if (rowCount == rowStart.length) {
|
||||
final int n = rowCount * 2;
|
||||
rowStart = Arrays.copyOf(rowStart, n);
|
||||
rowEnd = Arrays.copyOf(rowEnd, n);
|
||||
}
|
||||
rowStart[rowCount] = start;
|
||||
rowEnd[rowCount] = end;
|
||||
rowCount++;
|
||||
final int w = (int) Math.ceil(rowWidth);
|
||||
if (w > width) width = w;
|
||||
}
|
||||
|
||||
CharSequence rowText() {
|
||||
return isWrapped ? wrapped : text;
|
||||
}
|
||||
}
|
||||
|
||||
public final DebugGroup group;
|
||||
private Slot[] facts = NO_SLOTS;
|
||||
private int factCount;
|
||||
private Slot[] lines = NO_SLOTS;
|
||||
private int lineCount;
|
||||
private int measuredFactCount;
|
||||
private int measuredLineCount;
|
||||
private String title = "";
|
||||
private int titleWidth;
|
||||
private boolean titleDirty;
|
||||
private int factNameWidth;
|
||||
private int fullWidth;
|
||||
private int fullHeight;
|
||||
private boolean wasEmpty = true;
|
||||
private int widthHeldSince;
|
||||
private int heightHeldSince;
|
||||
private boolean holdReleased;
|
||||
|
||||
public DebugGroupContents(DebugGroup group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
factCount = 0;
|
||||
lineCount = 0;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return factCount == 0 && lineCount == 0;
|
||||
}
|
||||
|
||||
public int lineCount() {
|
||||
return lineCount;
|
||||
}
|
||||
|
||||
public int factCount() {
|
||||
return factCount;
|
||||
}
|
||||
|
||||
public Slot fact(int i) {
|
||||
return facts[i];
|
||||
}
|
||||
|
||||
public Slot line(int i) {
|
||||
return lines[i];
|
||||
}
|
||||
|
||||
public int fullWidth() {
|
||||
return fullWidth;
|
||||
}
|
||||
|
||||
public int fullHeight() {
|
||||
return fullHeight;
|
||||
}
|
||||
|
||||
void releaseSizeHold() {
|
||||
holdReleased = true;
|
||||
}
|
||||
|
||||
void setTitle(String title, int width) {
|
||||
if (this.title.equals(title) && titleWidth == width) return;
|
||||
this.title = title;
|
||||
this.titleWidth = width;
|
||||
titleDirty = true;
|
||||
}
|
||||
|
||||
private static Slot[] grow(Slot[] slots, int count) {
|
||||
if (count < slots.length) return slots;
|
||||
final int n = Math.max(4, count * 2);
|
||||
final Slot[] grown = Arrays.copyOf(slots, n);
|
||||
for (int i = count; i < n; i++) grown[i] = new Slot();
|
||||
return grown;
|
||||
}
|
||||
|
||||
Slot addFact(String name) {
|
||||
facts = grow(facts, factCount);
|
||||
final Slot slot = facts[factCount++];
|
||||
if (slot.name != name) {
|
||||
slot.name = name;
|
||||
slot.label = name.concat(":");
|
||||
slot.nameWidth = -1;
|
||||
}
|
||||
slot.begin();
|
||||
return slot;
|
||||
}
|
||||
|
||||
Slot addLine() {
|
||||
lines = grow(lines, lineCount);
|
||||
final Slot slot = lines[lineCount++];
|
||||
slot.begin();
|
||||
return slot;
|
||||
}
|
||||
|
||||
int measure(DebugText renderer, int lineBudget, boolean force, int tick) {
|
||||
boolean text = force || titleDirty || factCount != measuredFactCount || lineCount != measuredLineCount;
|
||||
titleDirty = false;
|
||||
measuredFactCount = factCount;
|
||||
measuredLineCount = lineCount;
|
||||
int nameWidth = 0;
|
||||
for (int i = 0; i < factCount; i++) {
|
||||
final Slot slot = facts[i];
|
||||
if (force || slot.nameWidth < 0) {
|
||||
slot.nameWidth = renderer.width(slot.name, 0, slot.name.length());
|
||||
text = true;
|
||||
}
|
||||
if (slot.nameWidth > nameWidth) nameWidth = slot.nameWidth;
|
||||
}
|
||||
if (nameWidth != factNameWidth) text = true;
|
||||
factNameWidth = nameWidth;
|
||||
final int valueBudget = Math.max(lineBudget - nameWidth - FACT_PADDING, MIN_VALUE_BUDGET);
|
||||
int rows = 0;
|
||||
int width = 0;
|
||||
for (int i = 0; i < factCount; i++) {
|
||||
final Slot slot = facts[i];
|
||||
text |= slot.measure(renderer, valueBudget, force);
|
||||
rows += slot.rowCount;
|
||||
final int w = nameWidth + FACT_PADDING + slot.width;
|
||||
if (w > width) width = w;
|
||||
}
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
final Slot slot = lines[i];
|
||||
text |= slot.measure(renderer, lineBudget, force);
|
||||
rows += slot.rowCount;
|
||||
if (slot.width > width) width = slot.width;
|
||||
}
|
||||
if (titleWidth > 0 && titleWidth + TITLE_PADDING > width) width = titleWidth + TITLE_PADDING;
|
||||
int height = rows * ROW_HEIGHT;
|
||||
if (titleWidth > 0) height += ROW_HEIGHT;
|
||||
if (factCount > 0 && lineCount > 0) height += FACT_LINE_GAP;
|
||||
final boolean empty = isEmpty();
|
||||
final boolean free = force || holdReleased;
|
||||
holdReleased = false;
|
||||
if (free || empty || width >= fullWidth || tick - widthHeldSince >= SHRINK_HOLD_TICKS) {
|
||||
widthHeldSince = tick;
|
||||
} else {
|
||||
width = fullWidth;
|
||||
}
|
||||
if (free || empty || height >= fullHeight || tick - heightHeldSince >= SHRINK_HOLD_TICKS) {
|
||||
heightHeldSince = tick;
|
||||
} else {
|
||||
height = fullHeight;
|
||||
}
|
||||
int flags = text ? TEXT : 0;
|
||||
if (width != fullWidth || height != fullHeight || empty != wasEmpty) flags |= SIZE;
|
||||
fullWidth = width;
|
||||
fullHeight = height;
|
||||
wasEmpty = empty;
|
||||
return flags;
|
||||
}
|
||||
|
||||
void emitRects(DebugText renderer, int left, int top, int side, int screenWidth) {
|
||||
int y = top;
|
||||
if (titleWidth > 0) {
|
||||
renderer.rect(left - 1, top - 1, left + fullWidth + 1, top + ROW_HEIGHT - 1, TITLE_BAR_COLOR);
|
||||
y = top + ROW_HEIGHT;
|
||||
}
|
||||
renderer.rect(left - 1, y - 1, left + fullWidth + 1, top + fullHeight + 1, BODY_COLOR);
|
||||
if (group.hasAccent()) {
|
||||
final int x = side == DebugGroup.LEFT ? 0 : screenWidth - 1;
|
||||
renderer.rect(x, top - 1, x + 1, top + fullHeight + 1, 0xFF000000 | group.accentColor);
|
||||
}
|
||||
}
|
||||
|
||||
void drawText(DebugText renderer, int left, int top) {
|
||||
int y = top;
|
||||
if (titleWidth > 0) {
|
||||
renderer.draw(title, 0, title.length(), left + TITLE_PADDING, top, TITLE_COLOR);
|
||||
y += ROW_HEIGHT;
|
||||
}
|
||||
final int valueX = left + factNameWidth + FACT_PADDING;
|
||||
for (int i = 0; i < factCount; i++) {
|
||||
final Slot slot = facts[i];
|
||||
renderer.draw(slot.label, 0, slot.label.length(), left + (factNameWidth - slot.nameWidth), y, NAME_COLOR);
|
||||
y = drawRows(renderer, slot, valueX, y, VALUE_COLOR);
|
||||
}
|
||||
if (factCount > 0 && lineCount > 0) y += FACT_LINE_GAP;
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
y = drawRows(renderer, lines[i], left, y, LINE_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
private static int drawRows(DebugText renderer, Slot slot, int x, int y, int color) {
|
||||
final CharSequence text = slot.rowText();
|
||||
for (int row = 0; row < slot.rowCount; row++) {
|
||||
final int start = slot.rowStart[row];
|
||||
final int end = slot.rowEnd[row];
|
||||
if (end > start) renderer.draw(text, start, end, x, y, color);
|
||||
y += ROW_HEIGHT;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
||||
public final class DebugGuiScale {
|
||||
|
||||
public static final int UNCHANGED = -1;
|
||||
public static final int AUTO = 0;
|
||||
|
||||
private DebugGuiScale() {}
|
||||
|
||||
public static boolean evenForUnicode(Minecraft mc) {
|
||||
return mc.func_152349_b() && !AngelicaConfig.removeUnicodeEvenScaling;
|
||||
}
|
||||
|
||||
public static int maxScale(int displayWidth, int displayHeight, boolean evenForUnicode) {
|
||||
int scale = 1;
|
||||
while (displayWidth / (scale + 1) >= 320 && displayHeight / (scale + 1) >= 240) {
|
||||
scale++;
|
||||
}
|
||||
if (evenForUnicode && scale % 2 != 0 && scale != 1) scale--;
|
||||
return scale;
|
||||
}
|
||||
|
||||
public static int offeredMax(int maxScale, int gameScale) {
|
||||
return Math.max(1, Math.min(maxScale, gameScale));
|
||||
}
|
||||
|
||||
public static int resolve(int setting, int gameScale, int displayWidth, int displayHeight, boolean evenForUnicode) {
|
||||
if (setting == UNCHANGED) return gameScale;
|
||||
final int max = maxScale(displayWidth, displayHeight, evenForUnicode);
|
||||
final int wanted = setting == AUTO ? Math.max(1, max / 2) : Math.min(setting, max);
|
||||
return Math.max(1, Math.min(wanted, gameScale));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
public final class DebugLayout {
|
||||
|
||||
public static final int MARGIN_TOP = 2;
|
||||
public static final int GROUP_SPACING = 9;
|
||||
|
||||
public final int[] height;
|
||||
public final int[] side;
|
||||
public final int[] top;
|
||||
public final int[] columnHeight = new int[2];
|
||||
|
||||
private final int[] maxHeight = new int[2];
|
||||
private final int[] homeSide;
|
||||
private final int count;
|
||||
private final int priority;
|
||||
|
||||
public DebugLayout(DebugRegistry registry) {
|
||||
count = registry.groupCount();
|
||||
height = new int[count];
|
||||
side = new int[count];
|
||||
top = new int[count];
|
||||
homeSide = new int[count];
|
||||
for (int g = 0; g < count; g++) {
|
||||
homeSide[g] = registry.groupAt(g).side;
|
||||
}
|
||||
final DebugGroup priorityGroup = registry.groupById(DebugGroup.PRIORITY_LEFT_ID);
|
||||
priority = priorityGroup == null ? -1 : priorityGroup.index;
|
||||
}
|
||||
|
||||
private void place(int group, int column) {
|
||||
top[group] = columnHeight[column];
|
||||
side[group] = column;
|
||||
columnHeight[column] += height[group] + GROUP_SPACING;
|
||||
}
|
||||
|
||||
private boolean isFull(int column) {
|
||||
return columnHeight[column] >= maxHeight[column];
|
||||
}
|
||||
|
||||
private boolean fits(int group, int column) {
|
||||
return columnHeight[column] + height[group] <= maxHeight[column];
|
||||
}
|
||||
|
||||
public void assign(int top, int leftMaxHeight, int rightMaxHeight) {
|
||||
columnHeight[DebugGroup.LEFT] = top;
|
||||
columnHeight[DebugGroup.RIGHT] = top;
|
||||
maxHeight[DebugGroup.LEFT] = leftMaxHeight;
|
||||
maxHeight[DebugGroup.RIGHT] = rightMaxHeight;
|
||||
for (int g = 0; g < count; g++) {
|
||||
side[g] = -1;
|
||||
}
|
||||
if (priority >= 0 && height[priority] > 0) place(priority, homeSide[priority]);
|
||||
for (int g = 0; g < count; g++) {
|
||||
if (height[g] <= 0 || side[g] >= 0) continue;
|
||||
final int column = homeSide[g];
|
||||
if (fits(g, column)) place(g, column);
|
||||
}
|
||||
for (int g = 0; g < count; g++) {
|
||||
if (height[g] <= 0 || side[g] >= 0) continue;
|
||||
final int column = 1 - homeSide[g];
|
||||
if (fits(g, column)) place(g, column);
|
||||
}
|
||||
for (int g = 0; g < count; g++) {
|
||||
if (height[g] <= 0 || side[g] >= 0) continue;
|
||||
final int column = homeSide[g];
|
||||
if (!isFull(column)) place(g, column);
|
||||
else if (!isFull(1 - column)) place(g, 1 - column);
|
||||
}
|
||||
}
|
||||
|
||||
public void assignSingleColumn(int column, int columnTop, int columnMaxHeight) {
|
||||
columnHeight[column] = columnTop;
|
||||
maxHeight[column] = columnMaxHeight;
|
||||
for (int g = 0; g < count; g++) {
|
||||
side[g] = -1;
|
||||
}
|
||||
if (priority >= 0 && height[priority] > 0) place(priority, column);
|
||||
for (int g = 0; g < count; g++) {
|
||||
if (height[g] <= 0 || side[g] >= 0) continue;
|
||||
if (!isFull(column)) place(g, column);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class DebugLineRules {
|
||||
|
||||
private static final int MAX_PREFIX = 64;
|
||||
private static final Pattern RULE = Pattern.compile("\\s*(drop|fact)\\s+(left|right)\\s+\"([^\"]*)\"((?:\\s+\\w+=(?:\"[^\"]*\"|[^\\s\"]+))*)\\s*");
|
||||
private static final Pattern OPTION = Pattern.compile("(\\w+)=(?:\"([^\"]*)\"|([^\\s\"]+))");
|
||||
|
||||
static final String[] DEFAULTS = {
|
||||
"drop right \"Allocation rate: \" when=minecraft:memory",
|
||||
"drop right \"Java: \" when=minecraft:system_specs",
|
||||
"drop right \"GPU: \" when=minecraft:system_specs",
|
||||
"drop right \"OpenGL: \" when=minecraft:system_specs",
|
||||
"drop right \"CPU Cores: \" when=minecraft:system_specs",
|
||||
"fact right \"IntCache: \" group=angelica:memory when=minecraft:memory",
|
||||
"fact right \"OS: \" group=angelica:system_specs when=minecraft:system_specs",
|
||||
"fact right \"LWJGL: \" group=angelica:system_specs when=minecraft:system_specs",
|
||||
"fact left \"Cube X: \" group=angelica:position when=minecraft:player_position",
|
||||
"fact left \"Cube Y: \" group=angelica:position when=minecraft:player_position",
|
||||
"fact left \"Cube Z: \" group=angelica:position when=minecraft:player_position",
|
||||
"fact left \"Cube: \" group=angelica:position when=minecraft:player_position",
|
||||
"fact left \"hunger: \" group=angelica:position when=minecraft:player_position name=\"Hunger\"",
|
||||
"fact right \"HydroEnergy GPU RAM: \" group=angelica:memory when=minecraft:memory",
|
||||
"fact right \"renderDebugMode: \" group=angelica:rendering when=minecraft:chunk_render_stats name=\"Render Debug Mode\"",
|
||||
};
|
||||
|
||||
public record Rule(String prefix, DebugEntryDef when, DebugGroup group, String name) {
|
||||
|
||||
int payloadStart(String line) {
|
||||
int i = matchPrefix(line, skipCodes(line, 0), prefix);
|
||||
while (i < line.length() && line.charAt(i) == ' ') i++;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
private final Rule[][] rules;
|
||||
|
||||
private DebugLineRules(Rule[][] rules) {
|
||||
this.rules = rules;
|
||||
}
|
||||
|
||||
static DebugLineRules compile(DebugRegistry registry, String[] configRules) {
|
||||
final ArrayList<Rule> left = new ArrayList<>();
|
||||
final ArrayList<Rule> right = new ArrayList<>();
|
||||
if (configRules != null) {
|
||||
for (String text : configRules) add(registry, text, left, right);
|
||||
}
|
||||
for (String text : DEFAULTS) add(registry, text, left, right);
|
||||
return new DebugLineRules(new Rule[][] { left.toArray(new Rule[0]), right.toArray(new Rule[0]) });
|
||||
}
|
||||
|
||||
private static void add(DebugRegistry registry, String text, ArrayList<Rule> left, ArrayList<Rule> right) {
|
||||
final Matcher matcher = text == null ? null : RULE.matcher(text);
|
||||
final Rule rule = matcher != null && matcher.matches() ? parse(registry, matcher) : null;
|
||||
if (rule == null) {
|
||||
AngelicaMod.LOGGER.warn("invalid debug line rule: {}", text);
|
||||
} else {
|
||||
(matcher.group(2).equals("left") ? left : right).add(rule);
|
||||
}
|
||||
}
|
||||
|
||||
private static Rule parse(DebugRegistry registry, Matcher rule) {
|
||||
final String prefix = stripCodes(rule.group(3));
|
||||
if (prefix.isEmpty() || prefix.length() > MAX_PREFIX) return null;
|
||||
String groupId = null;
|
||||
String name = null;
|
||||
String whenId = null;
|
||||
final Matcher option = OPTION.matcher(rule.group(4));
|
||||
while (option.find()) {
|
||||
final String value = option.group(2) != null ? option.group(2) : option.group(3);
|
||||
switch (option.group(1)) {
|
||||
case "group" -> groupId = value;
|
||||
case "name" -> name = value;
|
||||
case "when" -> whenId = value;
|
||||
default -> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
final DebugEntryDef when = whenId == null ? null : registry.entryById(whenId);
|
||||
if (whenId != null && when == null) return null;
|
||||
if (rule.group(1).equals("drop")) return groupId == null && name == null ? new Rule(prefix, when, null, null) : null;
|
||||
final DebugGroup group = groupId == null ? null : registry.groupById(groupId);
|
||||
if (name == null) name = defaultName(prefix);
|
||||
return group == null || name.isEmpty() ? null : new Rule(prefix, when, group, name);
|
||||
}
|
||||
|
||||
private static String stripCodes(String s) {
|
||||
if (s.indexOf(DebugText.FORMAT) < 0) return s;
|
||||
final StringBuilder sb = new StringBuilder(s.length());
|
||||
DebugText.appendStripped(sb, s);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String defaultName(String prefix) {
|
||||
int end = prefix.length();
|
||||
while (end > 0 && prefix.charAt(end - 1) == ' ') end--;
|
||||
if (end > 0 && prefix.charAt(end - 1) == ':') end--;
|
||||
return prefix.substring(0, end);
|
||||
}
|
||||
|
||||
static int skipCodes(String s, int from) {
|
||||
final int last = s.length() - 1;
|
||||
int i = from;
|
||||
while (i < last && s.charAt(i) == DebugText.FORMAT) i += 2;
|
||||
return i;
|
||||
}
|
||||
|
||||
static int matchPrefix(String line, int rawStart, String prefix) {
|
||||
final int length = line.length();
|
||||
int i = rawStart;
|
||||
for (int p = 0, n = prefix.length(); p < n; p++) {
|
||||
i = skipCodes(line, i);
|
||||
if (i >= length || line.charAt(i) != prefix.charAt(p)) return -1;
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
static boolean isBlank(String line) {
|
||||
final int length = line.length();
|
||||
for (int i = skipCodes(line, 0); i < length; i = skipCodes(line, i + 1)) {
|
||||
if (line.charAt(i) > ' ') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Rule match(int side, String line) {
|
||||
final int start = skipCodes(line, 0);
|
||||
for (Rule rule : rules[side]) {
|
||||
if ((rule.when == null || rule.when.isEnabled()) && matchPrefix(line, start, rule.prefix) >= 0) return rule;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
public final class DebugNumbers {
|
||||
|
||||
private static final long[] POW10 = { 1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L };
|
||||
private static final double MAX_SCALED = 9.0e18;
|
||||
|
||||
private DebugNumbers() {}
|
||||
|
||||
public static void appendFixed(StringBuilder out, double value, int decimals) {
|
||||
final int places = Math.max(0, Math.min(decimals, POW10.length - 1));
|
||||
final long scale = POW10[places];
|
||||
final double magnitude = Math.abs(value) * scale;
|
||||
if (Double.isNaN(value) || Double.isInfinite(value) || magnitude >= MAX_SCALED) {
|
||||
out.append(value);
|
||||
return;
|
||||
}
|
||||
final long scaled = Math.round(magnitude);
|
||||
if (value < 0 && scaled != 0) out.append('-');
|
||||
out.append(scaled / scale);
|
||||
if (places == 0) return;
|
||||
out.append('.');
|
||||
appendPadded(out, scaled % scale, places);
|
||||
}
|
||||
|
||||
public static void appendPadded(StringBuilder out, long value, int width) {
|
||||
if (value >= 0) {
|
||||
int digits = 1;
|
||||
for (long rest = value / 10; rest > 0; rest /= 10) {
|
||||
digits++;
|
||||
}
|
||||
for (int i = digits; i < width; i++) {
|
||||
out.append('0');
|
||||
}
|
||||
}
|
||||
out.append(value);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 279 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user