Compare commits

...
8 Commits
Author SHA1 Message Date
Jason Mitchell b2f597a73a SDL presenter fixes and frame pacer adjustments (#2025) 2026-08-16 17:59:33 -07:00
Jason Mitchell 553dec5b6d Mark tiles at transform time instead of using reflection (#2024) 2026-08-16 16:05:04 -07:00
Jason Mitchell bc8116006c Tracy 0.14 (#2019) 2026-08-15 19:09:36 -07:00
Jason Mitchell bf9492dffb Defer ticking on TEs we eagerly create until they get a description packet (#2018) 2026-08-15 17:04:50 -07:00
Jason Mitchell 69aed42b89 Sign Fix (#2016) 2026-08-15 14:32:50 -07:00
KAMKEELandJason Mitchell ffe54f61b9 Fixes World Lighting to Text Rendering so its not Fullbright (#1976)
* Fixes World Lighting to Text Rendering so its not Fullbright

* Carry lightmap state per text segment

* Text lighting: per-segment lightmap uniform, fix GL_RESCALE_NORMAL
* per segment uniform instead of stride increase
* Shade text based on GL light state
* fix u_NormalScale
* GL_LIGHT > 1 usage warnings (not currently handled)

---------

Co-authored-by: Jason Mitchell <[email protected]>
2026-08-14 21:34:58 -07:00
Jason Mitchell c86e1de9f6 TE and world access thread safety for rendering, and remesh on first TE update packet. (#2014) 2026-08-14 20:21:13 -07:00
brandyn 6af401c834 fix starminer rotation & optimize starminer rotateCorpseByGravity (#2005)
* fix starminer rotation & optimize starminer rotateCorpseByGravity

fixes #487

* remove Object[] allocation

* add shared helper,  fix NPEs, fallbacks
2026-08-13 11:27:43 -07:00
82 changed files with 2609 additions and 545 deletions

No files matched your search

+5 -3
View File
@@ -2,7 +2,8 @@
# Builds the libTracyClient shared library for the tracy-client module.
set -euo pipefail
TRACY_VERSION="${TRACY_VERSION:-v0.13.1}"
TRACY_VERSION="${TRACY_VERSION:-v0.14.0}"
TRACY_VERSION_BARE="${TRACY_VERSION#v}"
OUT_DIR="${OUT_DIR:-dist/tracy-natives}"
TRACY_REPO="https://github.com/wolfpld/tracy"
@@ -16,7 +17,7 @@ case "$(uname -m)" in
arm64|aarch64) PLAT_ARCH="arm64" ;;
*) PLAT_ARCH="x64" ;;
esac
RES_DIR="${RES_DIR:-$REPO_ROOT/tracy-client/src/main/resources/natives/tracy/$PLAT_OS-$PLAT_ARCH}"
RES_DIR="${RES_DIR:-$REPO_ROOT/tracy-client/src/main/resources/natives/tracy/$TRACY_VERSION_BARE/$PLAT_OS-$PLAT_ARCH}"
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
@@ -33,7 +34,8 @@ fi
BUILD_DIR="$WORK_DIR/build"
export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-11.0}"
cmake -S "$SRC_DIR" -B "$BUILD_DIR" \
cmake -S "$REPO_ROOT/tracy-client/src/main/native" -B "$BUILD_DIR" \
-DTRACY_SRC_DIR="$SRC_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=ON \
-DTRACY_STATIC=OFF \
+12 -23
View File
@@ -6,10 +6,6 @@ on:
- "tracy-v*"
workflow_dispatch:
inputs:
tracy-version:
description: Tracy git tag to build
required: false
default: v0.13.1
publish:
description: Publish angelica-tracy.jar to the GTNH maven
type: boolean
@@ -20,25 +16,25 @@ jobs:
build:
strategy:
matrix:
platform: [linux-x64, windows-x64, macos-x64, macos-arm64]
include:
- os: ubuntu-22.04
platform: linux-x64
- os: windows-2025
platform: windows-x64
- os: macos-15-intel
platform: macos-x64
- os: macos-15
platform: macos-arm64
- platform: linux-x64
os: ubuntu-22.04
- platform: windows-x64
os: windows-2025
- platform: macos-x64
os: macos-15-intel
- platform: macos-arm64
os: macos-15
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- name: Build libTracyClient
shell: bash
env:
RAW_VERSION: ${{ github.ref_type == 'tag' && github.ref_name || inputs.tracy-version }}
OUT_DIR: dist/tracy-natives
run: |
export TRACY_VERSION="${RAW_VERSION#tracy-}"
export TRACY_VERSION="v$(sed -nE 's/^tracy = "(.*)"$/\1/p' gradle/libs.versions.toml)"
bash .github/scripts/build-tracy-natives.sh
- uses: actions/upload-artifact@v4
with:
@@ -56,14 +52,6 @@ jobs:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- name: Check tracy version matches the version catalog
shell: bash
env:
RAW_VERSION: ${{ github.ref_type == 'tag' && github.ref_name || inputs.tracy-version }}
run: |
ver="${RAW_VERSION#tracy-}"
grep -q "^tracy = \"${ver#v}\"$" gradle/libs.versions.toml \
|| { echo "libs.versions.toml tracy does not match $ver" >&2; exit 1; }
- uses: actions/download-artifact@v5
with:
pattern: tracy-natives-*
@@ -71,8 +59,9 @@ jobs:
- name: Stage natives into tracy-client resources
shell: bash
run: |
version="$(sed -nE 's/^tracy = "(.*)"$/\1/p' gradle/libs.versions.toml)"
for d in staging/tracy-natives-*; do
dest="tracy-client/src/main/resources/natives/tracy/${d#staging/tracy-natives-}"
dest="tracy-client/src/main/resources/natives/tracy/$version/${d#staging/tracy-natives-}"
mkdir -p "$dest"
cp "$d"/* "$dest/"
done
+2
View File
@@ -43,3 +43,5 @@ tracy-client/src/main/resources/natives/
tracy-client/angelica/
dist/tracy-natives/
*.tracy
tracy-client/angelica/
+3 -2
View File
@@ -52,8 +52,8 @@ configurations.configureEach {
}
DependencyHandlerScope.of(dependencies).apply {
compileOnly("com.github.GTNewHorizons:Hodgepodge:2.7.166:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Hodgepodge:2.7.166:dev")
compileOnly("com.github.GTNewHorizons:Hodgepodge:2.7.187:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Hodgepodge:2.7.187:dev")
runtimeOnlyNonPublishable(rfg.deobf("curse.maven:biomes-o-plenty-220318:2499612"))
@@ -202,6 +202,7 @@ afterEvaluate {
glsmTestFixtures(testFixtures(project(":glsm")) as ModuleDependency) { isTransitive = false }
testCompileOnly(testFixtures(project(":glsm")))
testImplementation("mega:fluidlogged-mc1.7.10:0.1.2")
testRuntimeOnly(libs.fastutil)
testRuntimeOnly(libs.joml)
@@ -58,7 +58,7 @@ public final class SystemProperties {
public static final boolean DEBUG_F3_DETAIL = Boolean.getBoolean("angelica.debug.f3Detail");
public static final boolean ENABLE_TEST_BLOCKS = Boolean.getBoolean("angelica.debug.testBlocks");
public static final boolean DUMP_CLASS = Boolean.getBoolean("angelica.debug.dumpClass");
public static final boolean DUMP_SHADERS = Boolean.getBoolean("angelica.debug.dumpShaders") || isDeobf();
private static final boolean DUMP_SHADERS_PROP = Boolean.getBoolean("angelica.debug.dumpShaders");
public static final boolean REDIRECTOR_LOGSPAM = Boolean.getBoolean("angelica.debug.redirectorLogspam");
public static final boolean DEBUG_DISPLAY_LISTS = Boolean.getBoolean("angelica.debug.displayLists");
public static final boolean LOG_DISPLAY_LIST_COMPILATION = Boolean.getBoolean("angelica.debug.displayLists.compilation");
@@ -68,8 +68,12 @@ public final class SystemProperties {
// Set by us, read by celeritas
public static final String KEY_CELERITAS_ENABLE_GL_DEBUG = "celeritas.enableGLDebug";
public static boolean dumpShaders() {
return DUMP_SHADERS_PROP || isDeobf();
}
public static Path shaderDumpDir(String phase) {
return DUMP_SHADERS ? Paths.get(SHADER_DUMP_ROOT, phase) : null;
return dumpShaders() ? Paths.get(SHADER_DUMP_ROOT, phase) : null;
}
public enum UnmappedGLMode {
@@ -118,7 +122,7 @@ public final class SystemProperties {
}
}
private static boolean isDeobf() {
public static boolean isDeobf() {
return Launch.blackboard != null && Boolean.TRUE.equals(Launch.blackboard.get("fml.deobfuscatedEnvironment"));
}
@@ -14,6 +14,7 @@ import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
import com.gtnewhorizons.angelica.glsm.ffp.VertexKey;
import com.gtnewhorizons.angelica.glsm.hooks.DeferredAlphaHandler;
import com.gtnewhorizons.angelica.glsm.hooks.DeferredBlendHandler;
import com.gtnewhorizons.angelica.glsm.hooks.DeferredDepthColorHandler;
@@ -3057,9 +3058,15 @@ public class GLStateManager {
return;
}
}
if (light >= VertexKey.FFP_LIGHT_COUNT && (warnedFfpLightMask & (1 << light)) == 0 && ShaderManager.getInstance().isEnabled()) {
warnedFfpLightMask |= (1 << light);
LOGGER.warn("GL_LIGHT{} enabled, but FFP shader emulation implements only GL_LIGHT0..{}", light, VertexKey.FFP_LIGHT_COUNT - 1, new Throwable("call site"));
}
ctx().lightStates[light].enable();
}
private static int warnedFfpLightMask = 0;
public static void enableColorMaterial() {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
@@ -0,0 +1,83 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.LightState;
import com.gtnewhorizons.angelica.glsm.states.MaterialState;
import org.joml.Matrix3f;
import org.joml.Vector3f;
import org.joml.Vector4f;
public final class FFPVertexLighting {
private static final Vector3f scratchNormal = new Vector3f();
private static final Vector3f scratchLightVec = new Vector3f();
private static final Vector4f scratchEyePos = new Vector4f();
private static final Matrix3f scratchNormalMat = new Matrix3f();
private static final Vector3f scratchVec = new Vector3f();
private FFPVertexLighting() {}
public static boolean modulatesVertexColor(Vector3f destFactor) {
destFactor.set(1.0f);
if (!GLStateManager.getLightingState().isEnabled()) return false;
if (!GLStateManager.getColorMaterial().isEnabled()) return false;
if (VertexKey.encodeColorMaterialMode(GLStateManager.getColorMaterialParameter().getValue()) != VertexKey.CM_AMBIENT_AND_DIFFUSE) return false;
final MaterialState material = GLStateManager.getFrontMaterial();
if (material.emission.x != 0.0f || material.emission.y != 0.0f || material.emission.z != 0.0f)
return false;
final boolean materialHasSpecular = material.shininess > 0.0f && (material.specular.x != 0.0f || material.specular.y != 0.0f || material.specular.z != 0.0f);
final Vector4f lightModelAmbient = GLStateManager.getLightModel().ambient;
float r = lightModelAmbient.x;
float g = lightModelAmbient.y;
float b = lightModelAmbient.z;
final var lightEnables = GLStateManager.getLightStates();
final var lightData = GLStateManager.getLightDataStates();
boolean normalReady = false;
for (int i = 0; i < VertexKey.FFP_LIGHT_COUNT; i++) {
if (!lightEnables[i].isEnabled()) continue;
final LightState light = lightData[i];
if (materialHasSpecular && (light.specular.x != 0.0f || light.specular.y != 0.0f || light.specular.z != 0.0f))
return false;
if (!normalReady) {
transformCurrentNormal();
normalReady = true;
}
final float nDotVP = Math.max(scratchNormal.dot(lightDirection(light)), 0.0f);
r += light.ambient.x + nDotVP * light.diffuse.x;
g += light.ambient.y + nDotVP * light.diffuse.y;
b += light.ambient.z + nDotVP * light.diffuse.z;
}
if (r == 1.0f && g == 1.0f && b == 1.0f) return false;
if (!Float.isFinite(r + g + b)) return false;
destFactor.set(r, g, b);
return true;
}
private static void transformCurrentNormal() {
GLStateManager.getModelViewMatrix().normal(scratchNormalMat);
scratchNormal.set(ShaderManager.getCurrentNormal());
scratchNormalMat.transform(scratchNormal);
if (GLStateManager.getNormalizeState().isEnabled()) {
scratchNormal.normalize();
} else if (GLStateManager.getRescaleNormalState().isEnabled()) {
scratchNormal.mul(Uniforms.rescaleFactor(scratchNormalMat, scratchVec));
}
}
private static Vector3f lightDirection(LightState light) {
if (light.position.w == 0.0f) {
scratchLightVec.set(light.position.x, light.position.y, light.position.z);
} else {
scratchEyePos.set(0.0f, 0.0f, 0.0f, 1.0f).mul(GLStateManager.getModelViewMatrix());
scratchLightVec.set(light.position.x - scratchEyePos.x, light.position.y - scratchEyePos.y, light.position.z - scratchEyePos.z);
}
return scratchLightVec.normalize();
}
}
@@ -232,8 +232,12 @@ public class Uniforms {
private void stageNormalMatrix() {
GLStateManager.getModelViewMatrix().normal(normalMatrix);
putMat3(FFPUniformBlock.NORMAL_MATRIX, normalMatrix);
final float scale = 1.0f / normalMatrix.getColumn(0, tempVec3).length();
putFloat(FFPUniformBlock.NORMAL_SCALE, scale);
putFloat(FFPUniformBlock.NORMAL_SCALE, rescaleFactor(normalMatrix, tempVec3));
}
static float rescaleFactor(Matrix3f normalMatrix, Vector3f scratch) {
final float f = normalMatrix.getColumn(2, scratch).lengthSquared();
return f > 1.0e-12f ? 1.0f / (float) Math.sqrt(f) : 1.0f;
}
private void stageTextureMatrices() {
@@ -20,38 +20,36 @@ public final class VertexKey {
public static final int MAX_UNITS = 4;
public static final int FFP_LIGHT_COUNT = 2;
private final long packed;
private static final int BIT_LIGHTING = 0;
private static final int BIT_LIGHT0 = 1;
private static final int BIT_LIGHT1 = 2;
private static final int BIT_LIGHT0_DIR = 3;
private static final int BIT_LIGHT1_DIR = 4;
private static final int BIT_COLOR_MATERIAL = 5;
private static final int BIT_SEPARATE_SPECULAR = 6;
private static final int BIT_FOG = 7;
private static final int BIT_FOG_DIST_MODE = 8; // 2 bits: 8-9
private static final int BIT_NORMALIZE = 10;
private static final int BIT_RESCALE_NORMAL = 11;
// Per-unit texcoord enable: bits 12-15 (unit 0..3). Unit 1 doubles as the lightmap flag.
private static final int BIT_UNIT_TEX_BASE = 12;
private static final int BIT_HAS_VERTEX_COLOR = 16;
private static final int BIT_HAS_VERTEX_NORMAL = 17;
private static final int BIT_HAS_VERTEX_TEX = 18;
private static final int BIT_HAS_VERTEX_LIGHTMAP = 19;
private static final int BIT_COLOR_MAT_MODE = 20; // 3 bits: 20-22
// TexGen modes (unit 0 only, for now): 3 bits each (0=NONE, 1=OBJ_LINEAR, 2=EYE_LINEAR).
private static final int BIT_TEXGEN_S = 23; // 3 bits: 23-25
private static final int BIT_TEXGEN_T = 26; // 3 bits: 26-28
private static final int BIT_TEXGEN_R = 29; // 3 bits: 29-31
private static final int BIT_TEXGEN_Q = 32; // 3 bits: 32-34
private static final int BIT_CLIP_PLANES = 35;
private static final int BIT_WIDE_LINE = 36;
// Per-unit texmat enable: bits 37-40 (unit 0..3). Unit 1 is the lightmap matrix.
private static final int BIT_UNIT_TEXMAT_BASE = 37;
private static final int BIT_UNIT23_UV_FROM_UNIT0 = 41;
private static final int BIT_LINE_STIPPLE = 42;
private static final int BIT_INSTANCED = 44;
private static final int BIT_LIGHT_BASE = BIT_LIGHTING + 1;
private static final int BIT_LIGHT_DIR_BASE = BIT_LIGHT_BASE + FFP_LIGHT_COUNT;
private static final int BIT_COLOR_MATERIAL = BIT_LIGHT_DIR_BASE + FFP_LIGHT_COUNT;
private static final int BIT_SEPARATE_SPECULAR = BIT_COLOR_MATERIAL + 1;
private static final int BIT_FOG = BIT_SEPARATE_SPECULAR + 1;
private static final int BIT_FOG_DIST_MODE = BIT_FOG + 1;
private static final int BIT_NORMALIZE = BIT_FOG_DIST_MODE + 2;
private static final int BIT_RESCALE_NORMAL = BIT_NORMALIZE + 1;
private static final int BIT_UNIT_TEX_BASE = BIT_RESCALE_NORMAL + 1;
private static final int BIT_HAS_VERTEX_COLOR = BIT_UNIT_TEX_BASE + MAX_UNITS;
private static final int BIT_HAS_VERTEX_NORMAL = BIT_HAS_VERTEX_COLOR + 1;
private static final int BIT_HAS_VERTEX_TEX = BIT_HAS_VERTEX_NORMAL + 1;
private static final int BIT_HAS_VERTEX_LIGHTMAP = BIT_HAS_VERTEX_TEX + 1;
private static final int BIT_COLOR_MAT_MODE = BIT_HAS_VERTEX_LIGHTMAP + 1;
// TexGen modes are unit 0 only, for now.
private static final int BIT_TEXGEN_S = BIT_COLOR_MAT_MODE + 3;
private static final int BIT_TEXGEN_T = BIT_TEXGEN_S + 3;
private static final int BIT_TEXGEN_R = BIT_TEXGEN_T + 3;
private static final int BIT_TEXGEN_Q = BIT_TEXGEN_R + 3;
private static final int BIT_CLIP_PLANES = BIT_TEXGEN_Q + 3;
private static final int BIT_WIDE_LINE = BIT_CLIP_PLANES + 1;
private static final int BIT_UNIT_TEXMAT_BASE = BIT_WIDE_LINE + 1;
private static final int BIT_UNIT23_UV_FROM_UNIT0 = BIT_UNIT_TEXMAT_BASE + MAX_UNITS;
private static final int BIT_LINE_STIPPLE = BIT_UNIT23_UV_FROM_UNIT0 + 1;
private static final int BIT_INSTANCED = BIT_LINE_STIPPLE + 1;
public static final int TG_NONE = 0;
public static final int TG_OBJ_LINEAR = 1;
@@ -70,10 +68,8 @@ public final class VertexKey {
public long pack() { return packed; }
public boolean lightingEnabled() { return bit(BIT_LIGHTING); }
public boolean light0Enabled() { return bit(BIT_LIGHT0); }
public boolean light1Enabled() { return bit(BIT_LIGHT1); }
public boolean light0Directional() { return bit(BIT_LIGHT0_DIR); }
public boolean light1Directional() { return bit(BIT_LIGHT1_DIR); }
public boolean lightEnabled(int light) { return bit(BIT_LIGHT_BASE + light); }
public boolean lightDirectional(int light) { return bit(BIT_LIGHT_DIR_BASE + light); }
public boolean colorMaterialEnabled() { return bit(BIT_COLOR_MATERIAL); }
public boolean separateSpecular() { return bit(BIT_SEPARATE_SPECULAR); }
public boolean fogEnabled() { return bit(BIT_FOG); }
@@ -136,17 +132,12 @@ public final class VertexKey {
if (lighting) {
bits |= (1L << BIT_LIGHTING);
if (GLStateManager.getLightStates()[0].isEnabled()) {
bits |= (1L << BIT_LIGHT0);
for (int i = 0; i < FFP_LIGHT_COUNT; i++) {
if (!GLStateManager.getLightStates()[i].isEnabled()) continue;
bits |= (1L << (BIT_LIGHT_BASE + i));
// position.w == 0 means directional
if (GLStateManager.getLightDataStates()[0].position.w == 0.0f) {
bits |= (1L << BIT_LIGHT0_DIR);
}
}
if (GLStateManager.getLightStates()[1].isEnabled()) {
bits |= (1L << BIT_LIGHT1);
if (GLStateManager.getLightDataStates()[1].position.w == 0.0f) {
bits |= (1L << BIT_LIGHT1_DIR);
if (GLStateManager.getLightDataStates()[i].position.w == 0.0f) {
bits |= (1L << (BIT_LIGHT_DIR_BASE + i));
}
}
@@ -265,7 +256,7 @@ public final class VertexKey {
@Override
public String toString() {
return String.format("FFPVertexKey[0x%011X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%d%d%d%d texmat=%d%d%d%d col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b wline=%b inst=%b]",
packed, lightingEnabled(), light0Enabled(), light1Enabled(),
packed, lightingEnabled(), lightEnabled(0), lightEnabled(1),
colorMaterialEnabled(), fogEnabled(),
unitTexCoordEnabled(0)?1:0, unitTexCoordEnabled(1)?1:0, unitTexCoordEnabled(2)?1:0, unitTexCoordEnabled(3)?1:0,
unitTexMatEnabled(0)?1:0, unitTexMatEnabled(1)?1:0, unitTexMatEnabled(2)?1:0, unitTexMatEnabled(3)?1:0,
@@ -182,11 +182,10 @@ public final class VertexShaderGenerator {
}
// Per-light accumulation
if (key.light0Enabled()) {
emitLightContribution(sb, key, 0);
}
if (key.light1Enabled()) {
emitLightContribution(sb, key, 1);
for (int i = 0; i < VertexKey.FFP_LIGHT_COUNT; i++) {
if (key.lightEnabled(i)) {
emitLightContribution(sb, key, i);
}
}
sb.append(" v_Color = vec4(clamp(color0, 0.0, 1.0), outAlpha);\n");
@@ -198,7 +197,7 @@ public final class VertexShaderGenerator {
private static void emitLightContribution(StringBuilder sb, VertexKey key, int lightIndex) {
final String li = String.valueOf(lightIndex);
final boolean directional = (lightIndex == 0) ? key.light0Directional() : key.light1Directional();
final boolean directional = key.lightDirectional(lightIndex);
final String posUniform = "u_Light" + li + "Position";
sb.append(" { // Light ").append(li).append('\n');
@@ -0,0 +1,92 @@
package com.gtnewhorizons.angelica.glsm.profiling;
import java.util.ArrayList;
import java.util.List;
public final class SectionRegistry {
public interface Emitter {
long enter(int category, String text);
void leave(long nativeId);
void setup(int category, String name);
boolean isConnected();
}
private static final class Category {
final int id;
final String name;
Category(int id, String name) {
this.id = id;
this.name = name;
}
}
private static final class Section {
final long token;
final int category;
final String text;
long nativeId;
Section(long token, int category, String text, long nativeId) {
this.token = token;
this.category = category;
this.text = text;
this.nativeId = nativeId;
}
}
private final Emitter emitter;
private final List<Category> categories = new ArrayList<>();
private final List<Section> open = new ArrayList<>();
private long nextToken;
private boolean connected;
public SectionRegistry(Emitter emitter) {
this.emitter = emitter;
}
public synchronized void registerCategory(int category, String name) {
categories.add(new Category(category, name));
}
public synchronized long enter(int category, String text) {
syncConnection();
final long token = ++nextToken;
open.add(new Section(token, category, text, emitter.enter(category, text)));
return token;
}
public synchronized void leave(long token) {
for (int i = 0; i < open.size(); i++) {
final Section section = open.get(i);
if (section.token == token) {
open.remove(i);
if (section.nativeId != 0L) emitter.leave(section.nativeId);
return;
}
}
}
public synchronized void poll() {
syncConnection();
}
private void syncConnection() {
final boolean now = emitter.isConnected();
if (now == connected) return;
connected = now;
if (now) replay();
}
private void replay() {
for (int i = 0; i < categories.size(); i++) {
final Category category = categories.get(i);
emitter.setup(category.id, category.name);
}
for (int i = 0; i < open.size(); i++) {
final Section section = open.get(i);
section.nativeId = emitter.enter(section.category, section.text);
}
}
}
@@ -44,6 +44,18 @@ public final class Tracy {
public static final int COLOR_IRIS = 0x9E862E; // gold
public static final int COLOR_FFP = 0x7A4E9E; // purple
public static final int SECTION_WORLD = 1;
public static final int SECTION_SHADERS = 2;
public static final int SECTION_BENCHMARK = 3;
public static final int SECTION_UI = 4;
private static final SectionRegistry SECTIONS = new SectionRegistry(new SectionRegistry.Emitter() {
@Override public long enter(int category, String text) { return BACKEND.sectionEnter(category, text); }
@Override public void leave(long nativeId) { BACKEND.sectionLeave(nativeId); }
@Override public void setup(int category, String name) { BACKEND.sectionSetup(category, name); }
@Override public boolean isConnected() { return BACKEND.isConnected(); }
});
private static final ThreadLocal<ZoneStack> STACK = new ThreadLocal<>() {
@Override
protected ZoneStack initialValue() {
@@ -62,6 +74,10 @@ public final class Tracy {
FINE_ZONES = ENABLED && SystemProperties.TRACY_FINE_ZONES;
if (ENABLED) {
Runtime.getRuntime().addShutdownHook(new Thread(Tracy::shutdown, "Tracy-Shutdown"));
SECTIONS.registerCategory(SECTION_WORLD, "World");
SECTIONS.registerCategory(SECTION_SHADERS, "Shaders");
SECTIONS.registerCategory(SECTION_BENCHMARK, "Benchmark");
SECTIONS.registerCategory(SECTION_UI, "UI");
LOGGER.info("Tracy profiling enabled");
}
}
@@ -95,6 +111,7 @@ public final class Tracy {
LOGGER.warn("Tracy requested (-Dangelica.tracy=true) but no TracyBackend service present");
return null;
}
if (!implementsCurrentAbi(found)) return null;
final TracyBackend backend = found;
final CountDownLatch done = new CountDownLatch(1);
final AtomicReference<Boolean> result = new AtomicReference<>();
@@ -120,10 +137,28 @@ public final class Tracy {
return backend;
}
private static final String ABI_SENTINEL = "sectionEnter";
private static boolean implementsCurrentAbi(TracyBackend backend) {
try {
backend.getClass().getMethod(ABI_SENTINEL, int.class, String.class);
return true;
} catch (NoSuchMethodException e) {
LOGGER.warn("Tracy backend {} was built against an older TracyBackend interface; profiling disabled", backend.getClass().getName());
return false;
}
}
private record InitRunner(TracyBackend backend, CountDownLatch done, AtomicReference<Boolean> result) implements Runnable {
@Override
public void run() {
final boolean ok = backend.init();
boolean ok;
try {
ok = backend.init();
} catch (LinkageError e) {
LOGGER.warn("Tracy backend {} failed to link; profiling disabled: {}", backend.getClass().getName(), e);
ok = false;
}
if (!result.compareAndSet(null, ok) && ok) {
backend.shutdown();
}
@@ -168,7 +203,7 @@ public final class Tracy {
final String name = stack.poppedName();
if (name != null && WARNED_LEAKS.add(name)) {
LOGGER.warn("Tracy: unbalanced profiler section '{}' left open (leaked by its caller); closed at next root", name);
BACKEND.message("unbalanced profiler section '" + name + "' closed at root");
BACKEND.message("unbalanced profiler section '" + name + "' closed at root", TracyBackend.SEVERITY_WARNING);
}
}
}
@@ -274,6 +309,7 @@ public final class Tracy {
threadNameRefreshed = true;
BACKEND.setCurrentThreadName(Thread.currentThread().getName());
}
SECTIONS.poll();
BACKEND.frameMark();
}
@@ -330,8 +366,22 @@ public final class Tracy {
}
public static void message(String text) {
message(text, TracyBackend.SEVERITY_INFO);
}
public static void message(String text, int severity) {
if (!ENABLED || SHUTTING_DOWN || !CaptureGate.markersThisFrame) return;
BACKEND.message(text);
BACKEND.message(text, severity);
}
public static long sectionEnter(int category, String text) {
if (!ENABLED || SHUTTING_DOWN) return 0L;
return SECTIONS.enter(category, text);
}
public static void sectionLeave(long token) {
if (token == 0L || !ENABLED || SHUTTING_DOWN) return;
SECTIONS.leave(token);
}
public static boolean isConnected() {
@@ -5,6 +5,13 @@ public interface TracyBackend {
int PLOT_FORMAT_MEMORY = 1;
int PLOT_FORMAT_PERCENTAGE = 2;
int SEVERITY_TRACE = 0;
int SEVERITY_DEBUG = 1;
int SEVERITY_INFO = 2;
int SEVERITY_WARNING = 3;
int SEVERITY_ERROR = 4;
int SEVERITY_FATAL = 5;
boolean init();
long internSrcLoc(String name, int color);
@@ -24,7 +31,14 @@ public interface TracyBackend {
void plotInt(long namePtr, long value);
void plot(long namePtr, double value);
void message(String text);
void message(String text, int severity);
default void message(String text) { message(text, SEVERITY_INFO); }
long sectionEnter(int category, String text);
void sectionLeave(long id);
void sectionSetup(int category, String name);
void setCurrentThreadName(String name);
boolean isConnected();
void shutdown();
@@ -2,7 +2,7 @@ package com.gtnewhorizons.angelica.glsm.profiling;
public final class ZoneStack {
public static final long EMPTY = -1L;
static final int MAX_DEPTH = 512;
public static final int MAX_DEPTH = 512;
private long[] stack = new long[64];
private boolean[] gpu = new boolean[64];
@@ -0,0 +1,241 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.joml.Vector3f;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import static com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags.COLOR_BIT;
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 FFPVertexLightingGLTest {
private static final int POSITION_LOC = VertexFormatElement.Usage.POSITION.getAttributeLocation();
private static final int COLOR_LOC = VertexFormatElement.Usage.COLOR.getAttributeLocation();
private static final int STRIDE = 16;
private static final int VERTEX_COLOR = 0xFF808080;
private static final float VERTEX_CHANNEL = 0x80 / 255.0f;
private static final Vector3f IDENTITY_FACTOR = new Vector3f(1.0f, 1.0f, 1.0f);
private final Vector3f factor = new Vector3f();
private int vao;
private int vbo;
@BeforeEach
void setUpState() {
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
GLStateManager.glLoadIdentity();
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glLoadIdentity();
GLStateManager.glViewport(0, 0, 800, 600);
GLStateManager.disableDepthTest();
GLStateManager.disableCull();
GLStateManager.disableBlend();
GLStateManager.disableTexture();
GLStateManager.glNormal3f(0.0f, 0.0f, 1.0f);
standardItemLighting();
GLStateManager.glMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_EMISSION, floats(0.0f, 0.0f, 0.0f, 1.0f));
GLStateManager.glMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_SPECULAR, floats(0.0f, 0.0f, 0.0f, 1.0f));
GLStateManager.glMaterialf(GL11.GL_FRONT_AND_BACK, GL11.GL_SHININESS, 0.0f);
}
@AfterEach
void cleanup() {
GLStateManager.glDisable(GL11.GL_NORMALIZE);
GLStateManager.glDisable(GL12.GL_RESCALE_NORMAL);
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glLoadIdentity();
GLStateManager.glDisable(GL11.GL_LIGHTING);
GLStateManager.glDisable(GL11.GL_LIGHT0);
GLStateManager.glDisable(GL11.GL_LIGHT1);
GLStateManager.glDisable(GL11.GL_LIGHT2);
GLStateManager.glDisable(GL11.GL_COLOR_MATERIAL);
final ShaderManager sm = ShaderManager.getInstance();
if (sm.isActive()) sm.deactivate();
sm.disable();
GLStateManager.glBindVertexArray(0);
if (vbo != 0) {
GLStateManager.glDeleteBuffers(vbo);
vbo = 0;
}
if (vao != 0) {
GLStateManager.glDeleteVertexArrays(vao);
vao = 0;
}
}
private static void standardItemLighting() {
GLStateManager.glEnable(GL11.GL_LIGHTING);
GLStateManager.glEnable(GL11.GL_LIGHT0);
GLStateManager.glEnable(GL11.GL_LIGHT1);
GLStateManager.glEnable(GL11.GL_COLOR_MATERIAL);
GLStateManager.glColorMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_AMBIENT_AND_DIFFUSE);
GLStateManager.glLightModel(GL11.GL_LIGHT_MODEL_AMBIENT, floats(0.4f, 0.4f, 0.4f, 1.0f));
light(GL11.GL_LIGHT0, 0.2f, 1.0f, -0.7f);
light(GL11.GL_LIGHT1, -0.2f, 1.0f, 0.7f);
}
private static void light(int light, float x, float y, float z) {
GLStateManager.glLight(light, GL11.GL_POSITION, floats(x, y, z, 0.0f));
GLStateManager.glLight(light, GL11.GL_DIFFUSE, floats(0.6f, 0.6f, 0.6f, 1.0f));
GLStateManager.glLight(light, GL11.GL_AMBIENT, floats(0.0f, 0.0f, 0.0f, 1.0f));
GLStateManager.glLight(light, GL11.GL_SPECULAR, floats(0.0f, 0.0f, 0.0f, 1.0f));
}
private static String gateState() {
final var material = GLStateManager.getFrontMaterial();
return "lighting=" + GLStateManager.getLightingState().isEnabled()
+ " colorMaterial=" + GLStateManager.getColorMaterial().isEnabled()
+ " cmParam=0x" + Integer.toHexString(GLStateManager.getColorMaterialParameter().getValue())
+ " emission=" + material.emission
+ " specular=" + material.specular
+ " shininess=" + material.shininess
+ " lmAmbient=" + GLStateManager.getLightModel().ambient
+ " light0=" + GLStateManager.getLightStates()[0].isEnabled()
+ " light1=" + GLStateManager.getLightStates()[1].isEnabled();
}
private static FloatBuffer floats(float... values) {
final FloatBuffer buf = BufferUtils.createFloatBuffer(values.length);
buf.put(values).flip();
return buf;
}
private void buildTriangle() {
vao = GLStateManager.glGenVertexArrays();
GLStateManager.glBindVertexArray(vao);
final ByteBuffer data = BufferUtils.createByteBuffer(3 * STRIDE);
putVertex(data, -0.9f, -0.9f);
putVertex(data, 0.9f, -0.9f);
putVertex(data, 0.0f, 0.9f);
data.flip();
vbo = GLStateManager.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
GLStateManager.glEnableVertexAttribArray(POSITION_LOC);
GLStateManager.glVertexAttribPointer(POSITION_LOC, 3, GL11.GL_FLOAT, false, STRIDE, 0L);
GLStateManager.glEnableVertexAttribArray(COLOR_LOC);
GLStateManager.glVertexAttribPointer(COLOR_LOC, 4, GL11.GL_UNSIGNED_BYTE, true, STRIDE, 12L);
VAOManager.setCurrentVertexFlags(COLOR_BIT);
}
private static void putVertex(ByteBuffer buf, float x, float y) {
final int base = buf.position();
buf.putFloat(base, x).putFloat(base + 4, y).putFloat(base + 8, 0.0f);
buf.putInt(base + 12, VERTEX_COLOR);
buf.position(base + STRIDE);
}
private float[] drawAndReadCenter() {
buildTriangle();
final ShaderManager sm = ShaderManager.getInstance();
sm.enable();
sm.activate();
GLStateManager.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "FFP draw must not raise a GL error");
final ByteBuffer pixel = BufferUtils.createByteBuffer(4);
GL11.glReadPixels(400, 200, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixel);
return new float[] { (pixel.get(0) & 0xFF) / 255.0f, (pixel.get(1) & 0xFF) / 255.0f, (pixel.get(2) & 0xFF) / 255.0f };
}
private void assertSolverMatchesShader(String context) {
assertTrue(FFPVertexLighting.modulatesVertexColor(factor), context + ": expected a representable factor");
final float[] actual = drawAndReadCenter();
final float[] expected = { Math.min(1.0f, VERTEX_CHANNEL * factor.x), Math.min(1.0f, VERTEX_CHANNEL * factor.y), Math.min(1.0f, VERTEX_CHANNEL * factor.z) };
for (int c = 0; c < 3; c++) {
assertEquals(expected[c], actual[c], 2.0f / 255.0f, context + ": channel " + c + " solver=" + expected[c] + " shader=" + actual[c] + " factor=" + factor);
}
}
@Test
void faceOnNormalMatchesGeneratedShader() {
GLStateManager.glNormal3f(0.0f, 0.0f, 1.0f);
assertSolverMatchesShader("face-on normal");
}
@Test
void signNormalMatchesGeneratedShader() {
GLStateManager.glNormal3f(0.0f, 0.0f, -0.010416667f);
assertSolverMatchesShader("sign normal");
assertTrue(factor.x < 0.45f, "a 0.0104-long normal must stay at the 0.4 light model ambient floor rather than pick up 0.6 per light, got " + factor.x);
}
@Test
void tiltedNormalMatchesGeneratedShader() {
GLStateManager.glNormal3f(0.3f, 0.8f, 0.5f);
assertSolverMatchesShader("tilted normal");
}
@Test
void rescaleNormalMatchesGeneratedShader() {
GLStateManager.glEnable(GL12.GL_RESCALE_NORMAL);
GLStateManager.glScalef(1.0f, 2.0f, 1.0f);
GLStateManager.glNormal3f(0.0f, 1.0f, 0.0f);
assertSolverMatchesShader("rescale normal");
}
@Test
void normalizeMatchesGeneratedShader() {
GLStateManager.glEnable(GL11.GL_NORMALIZE);
GLStateManager.glScalef(1.0f, 2.0f, 1.0f);
GLStateManager.glNormal3f(0.0f, 1.0f, 0.0f);
assertSolverMatchesShader("normalize");
assertTrue(factor.x > 1.2f, "normalize restores the unit normal, which must pick up more diffuse than rescale's 0.5-long one, got " + factor.x);
}
@Test
void materialEmissionIsNotRepresentable() {
GLStateManager.glMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_EMISSION, floats(0.1f, 0.1f, 0.1f, 1.0f));
assertFalse(FFPVertexLighting.modulatesVertexColor(factor), "emission is additive, so the lit colour is not vertexColor * factor");
assertEquals(IDENTITY_FACTOR, factor, "a rejected solve must leave an identity factor");
}
@Test
void specularContributionIsNotRepresentable() {
GLStateManager.glMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_SPECULAR, floats(1.0f, 1.0f, 1.0f, 1.0f));
GLStateManager.glMaterialf(GL11.GL_FRONT_AND_BACK, GL11.GL_SHININESS, 16.0f);
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_SPECULAR, floats(1.0f, 1.0f, 1.0f, 1.0f));
assertFalse(FFPVertexLighting.modulatesVertexColor(factor), "specular is additive, so the lit colour is not vertexColor * factor");
}
@Test
void nonAmbientAndDiffuseColorMaterialIsNotRepresentable() {
GLStateManager.glColorMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_EMISSION);
assertFalse(FFPVertexLighting.modulatesVertexColor(factor), "only AMBIENT_AND_DIFFUSE makes the lit colour scale with the vertex colour");
}
@Test
void lightsBeyondTheFfpCountAreIgnored() {
assertTrue(FFPVertexLighting.modulatesVertexColor(factor), FFPVertexLightingGLTest::gateState);
final Vector3f twoLights = new Vector3f(factor);
light(GL11.GL_LIGHT2, 0.0f, 0.0f, 1.0f);
GLStateManager.glEnable(GL11.GL_LIGHT2);
assertTrue(FFPVertexLighting.modulatesVertexColor(factor), FFPVertexLightingGLTest::gateState);
assertEquals(twoLights, factor, "VertexShaderGenerator emits only GL_LIGHT0/GL_LIGHT1, so text must not see GL_LIGHT2 either");
}
}
@@ -0,0 +1,32 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class UniformsRescaleTest {
private static float rescaleFactorFor(float sx, float sy, float sz) {
final Matrix3f normalMatrix = new Matrix3f();
new Matrix4f().rotateY(0.7f).scale(sx, sy, sz).normal(normalMatrix);
return Uniforms.rescaleFactor(normalMatrix, new Vector3f());
}
@Test
void rescaleFactorUsesTheInverseModelviewBottomRow() {
assertEquals(8.0f, rescaleFactorFor(4.0f, 2.0f, 8.0f), 1.0e-5f, "wrong normal matrix column");
}
@Test
void uniformScaleIsRowIndependent() {
assertEquals(2.0f, rescaleFactorFor(2.0f, 2.0f, 2.0f), 1.0e-5f);
}
@Test
void singularMatrixFallsBackToUnitScale() {
assertEquals(1.0f, rescaleFactorFor(1.0f, 1.0f, 0.0f), 1.0e-5f, "a degenerate normal matrix must not divide by zero");
}
}
@@ -0,0 +1,113 @@
package com.gtnewhorizons.angelica.glsm.profiling;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SectionRegistryTest {
private static final class RecordingEmitter implements SectionRegistry.Emitter {
final List<String> setups = new ArrayList<>();
final List<String> enters = new ArrayList<>();
final List<Long> leaves = new ArrayList<>();
long nextId;
boolean connected;
@Override
public long enter(int category, String text) {
enters.add(category + ":" + text);
return connected ? ++nextId : 0L;
}
@Override
public void leave(long nativeId) {
leaves.add(nativeId);
}
@Override
public void setup(int category, String name) {
setups.add(category + ":" + name);
}
@Override
public boolean isConnected() {
return connected;
}
}
private final RecordingEmitter emitter = new RecordingEmitter();
private final SectionRegistry registry = new SectionRegistry(emitter);
private void reconnect() {
emitter.connected = false;
registry.poll();
emitter.connected = true;
registry.poll();
}
@Test
void tokensOutliveEveryReplayAndLeaveUsesTheNewestId() {
final long openedWhileDisconnected = registry.enter(1, "world");
assertNotEquals(0L, openedWhileDisconnected, "token must be valid while disconnected");
assertEquals(0L, emitter.nextId, "no native id while disconnected");
emitter.connected = true;
registry.poll();
final long openedWhileConnected = registry.enter(2, "shaders");
assertNotEquals(openedWhileDisconnected, openedWhileConnected);
reconnect();
emitter.leaves.clear();
registry.leave(openedWhileDisconnected);
registry.leave(openedWhileConnected);
assertEquals(List.of(3L, 4L), emitter.leaves, "leave must use the post-reconnect ids");
}
@Test
void aSectionEnteredOnTheConnectingFrameIsNotEnteredTwice() {
emitter.connected = true;
registry.enter(2, "shaders pack / Overworld");
registry.poll();
assertEquals(List.of("2:shaders pack / Overworld"), emitter.enters, "must not re-enter a section already live on this connection");
}
@Test
void reconnectReplaysEveryCategoryAndEveryOpenSection() {
registry.registerCategory(1, "World");
registry.registerCategory(4, "UI");
emitter.connected = true;
registry.poll();
registry.enter(1, "world");
registry.enter(4, "pause");
emitter.setups.clear();
emitter.enters.clear();
reconnect();
assertEquals(List.of("1:World", "4:UI"), emitter.setups, "all categories re-registered");
assertEquals(List.of("1:world", "4:pause"), emitter.enters, "all open sections re-entered");
}
@Test
void leftSectionsAreNotResurrectedByAReconnect() {
emitter.connected = true;
registry.poll();
final long token = registry.enter(1, "transient");
registry.leave(token);
emitter.enters.clear();
emitter.leaves.clear();
reconnect();
registry.leave(token);
assertTrue(emitter.enters.isEmpty(), "closed sections must not be re-entered");
assertTrue(emitter.leaves.isEmpty(), "closed tokens must not be left twice");
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ eventbus = "7.0-beta.15"
glslTransformLib = "0.2.0-33.g3ec381e"
antlr4Runtime = "4.13.2"
jcpp = "1.4.14"
tracy = "0.13.1"
tracy = "0.14.0"
retroFuturaBootstrap = "1.1.0"
lombok = "1.18.46"
@@ -88,7 +88,6 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import org.lwjgl.sdl.SDLGPU;
import org.lwjgl.sdl.SDLSurface;
import org.lwjgl.sdl.SDL_GPUBlitInfo;
import org.lwjgl.sdl.SDL_GPUBufferLocation;
import org.lwjgl.sdl.SDL_GPUTextureLocation;
@@ -111,7 +110,7 @@ public class SDLGPURenderBackend extends RenderBackend {
private final Device device = SDLGPUGate.device();
private final FrameManager frameManager = new FrameManager(device);
private Presenter presenter;
private volatile Presenter presenter;
private final ResourceManager resourceManager = new ResourceManager(device, frameManager);
private final Image3DClear image3DClear = new Image3DClear();
private final ShaderManager shaderManager = new ShaderManager(device);
@@ -155,7 +154,8 @@ public class SDLGPURenderBackend extends RenderBackend {
private volatile boolean shutdown;
private OffscreenTarget splashTarget;
private volatile OffscreenTarget splashTarget;
private volatile boolean splashTeardownPending;
private static final ThreadRegistry<ContextState> registeredStates = new ThreadRegistry<>(new ContextState[0]);
private static final ThreadLocal<ContextState> tlState = new ThreadLocal<>();
@@ -241,16 +241,12 @@ public class SDLGPURenderBackend extends RenderBackend {
});
GLSMHooks.LOADING_CHECKPOINT
.addListener(event -> {
if (Thread.currentThread() == GLStateManager.getMainThread()) {
if (!GLStateManager.isSplashComplete()) {
SplashDispatcher.tryDispatch(this, splashTarget);
} else if (splashTarget != null) {
SplashDispatcher.signalFrameReady();
SplashDispatcher.tryDispatch(this, splashTarget);
splashTarget.destroy(resourceManager);
splashTarget = null;
SplashDispatcher.reset();
if (!GLStateManager.isSplashComplete()) {
if (Thread.currentThread() == GLStateManager.getMainThread()) {
SplashDispatcher.tryDispatch(this, splashTarget, false);
}
} else if (splashTarget != null) {
splashTeardownPending = true;
}
if (event.requiresSync) {
frameManager.requestSyncFlushOnAllRegisteredFrames();
@@ -411,12 +407,12 @@ public class SDLGPURenderBackend extends RenderBackend {
@Override public boolean wantsDisplayUpdateGateTiming() { return false; }
@Override public long lastFrameGateNanos() {
if (presenter != null && presenter.isEngaged()) return frameManager.windowFrame().lastFrameGateNanos;
if (presenter != null) return frameManager.windowFrame().lastFrameGateNanos;
return frameManager.lastFrameGateNanos();
}
@Override public long lastFrameGateEndNanos() {
if (presenter != null && presenter.isEngaged()) return frameManager.windowFrame().lastFrameGateEndNanos;
if (presenter != null) return frameManager.windowFrame().lastFrameGateEndNanos;
return frameManager.lastFrameGateEndNanos();
}
@@ -520,8 +516,9 @@ public class SDLGPURenderBackend extends RenderBackend {
@Override public boolean handleMakeCurrent(Object drawable) {
if (!isSDLManagedDrawable(drawable)) return false;
Lwjgl3GLCapabilitiesShim.installOnCurrentThread(advertisedCapabilities());
if (splashTarget != null && Thread.currentThread() != GLStateManager.getMainThread()) {
s().boundFboId = splashTarget.fboId();
final OffscreenTarget splash = splashTarget;
if (splash != null && Thread.currentThread() != GLStateManager.getMainThread()) {
s().boundFboId = splash.fboId();
frameManager.frame().swapchainUnavailable = true;
}
return true;
@@ -542,7 +539,8 @@ public class SDLGPURenderBackend extends RenderBackend {
}
@Override public boolean handleSwapBuffers() {
if (splashTarget != null && splashTarget.isFor(s()) && Thread.currentThread() != GLStateManager.getMainThread()) {
final OffscreenTarget splash = splashTarget;
if (splash != null && splash.isFor(s()) && Thread.currentThread() != GLStateManager.getMainThread()) {
endFrameUploadFlush();
frameManager.endFrame();
SplashDispatcher.signalFrameReady((int) s().viewportW, (int) s().viewportH);
@@ -560,13 +558,24 @@ public class SDLGPURenderBackend extends RenderBackend {
final long drawnSize = SplashDispatcher.getDrawnSize();
final int srcW = Math.max(1, Math.min((int) (drawnSize >> 32), target.width()));
final int srcH = Math.max(1, Math.min((int) drawnSize, target.height()));
final FrameState f = frameManager.frame();
frameManager.presentBlit(f, target.colorTexture(), srcW, srcH, SDLSurface.SDL_FLIP_VERTICAL);
f.presentedThisFrame = false;
frameManager.presentSplash(target.colorTexture(), srcW, srcH);
}
private void finishSplash() {
splashTeardownPending = false;
final OffscreenTarget target = splashTarget;
if (target == null) return;
SplashDispatcher.signalFrameReady();
SplashDispatcher.tryDispatch(this, target, true);
if (presenter != null) presenter.drain();
splashTarget = null;
target.destroy(resourceManager);
SplashDispatcher.reset();
}
@Override public void onFrameBegin() {
if (shutdown) return;
if (splashTeardownPending) finishSplash();
if (GLStateManager.isSplashComplete()) applyPendingVSync();
frameManager.beginFrame();
beginFrameInit();
@@ -30,6 +30,7 @@ import java.util.concurrent.atomic.AtomicLong;
import static org.lwjgl.sdl.SDLGPU.*;
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_NONE;
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_VERTICAL;
import static org.lwjgl.system.MemoryStack.*;
/**
@@ -61,7 +62,6 @@ public final class FrameManager {
public boolean fbo0UsedThisFrame;
public boolean presentedThisFrame;
public boolean swapchainUnavailable;
public boolean windowThreadChecked;
public long renderPassGeneration;
public boolean wantFenceOnNextSubmit;
@@ -464,6 +464,20 @@ public final class FrameManager {
}
}
private volatile boolean acquireThreadReported;
private void checkAcquireOnWindowThread() {
if (acquireThreadReported) return;
final Thread self = Thread.currentThread();
final Thread window = device.getWindowThread();
if (window == null || window == self) return;
acquireThreadReported = true;
final String message = "Swapchain acquired on '" + self.getName() + "', window created on '" + window.getName() + "'";
if (SystemProperties.isDeobf() && !SystemProperties.DISABLE_SDL_PRESENTER_THREAD) throw new IllegalStateException(message);
LOG.warn(message);
}
public long ensureCopyPass() {
final FrameState f = frame();
if (f.copyPass != 0) {
@@ -863,7 +877,7 @@ public final class FrameManager {
return frame().lastFrameGateEndNanos;
}
private Presenter presenter;
private volatile Presenter presenter;
public void setPresenter(Presenter presenter) {
this.presenter = presenter;
@@ -877,18 +891,24 @@ public final class FrameManager {
public void presentFinalTarget() {
if (!finalTarget.hasContent()) return;
final long texture = finalTarget.colorTexture();
final int width = finalTarget.width();
final int height = finalTarget.height();
present(finalTarget.colorTexture(), finalTarget.width(), finalTarget.height(), SDL_FLIP_NONE, false);
}
public void presentSplash(long srcTexture, int srcW, int srcH) {
present(srcTexture, srcW, srcH, SDL_FLIP_VERTICAL, true);
}
private void present(long srcTexture, int srcW, int srcH, int flipMode, boolean splash) {
final FrameState f = frame();
if (f.presentedThisFrame) return;
final Presenter p = presenter;
if (p != null && p.isEngaged()) {
final FrameState f = frame();
if (f.presentedThisFrame) return;
if (p != null) {
f.presentedThisFrame = true;
p.requestPresent(texture, width, height);
p.requestPresent(srcTexture, srcW, srcH, flipMode);
} else {
presentBlit(frame(), texture, width, height, SDL_FLIP_NONE);
presentBlit(f, srcTexture, srcW, srcH, flipMode);
}
if (splash) f.presentedThisFrame = false;
}
private final AtomicLong windowAcquireWaitNanos = new AtomicLong();
@@ -915,8 +935,8 @@ public final class FrameManager {
else f.presentSkipsThisFrame++;
}
public void presentOnWindowThread(long srcTexture, int srcW, int srcH) {
presentBlit(windowFrame, srcTexture, srcW, srcH, SDL_FLIP_NONE);
public void presentOnWindowThread(long srcTexture, int srcW, int srcH, int flipMode) {
presentBlit(windowFrame, srcTexture, srcW, srcH, flipMode);
windowFrame.presentedThisFrame = false;
windowFrame.swapchainUnavailable = false;
}
@@ -925,16 +945,7 @@ public final class FrameManager {
if (srcTexture == 0 || srcW <= 0 || srcH <= 0) return false;
if (f.presentedThisFrame || f.swapchainUnavailable) return false;
if (!f.windowThreadChecked) {
f.windowThreadChecked = true;
final Thread self = Thread.currentThread();
final Thread windowThread = device.getWindowThread();
if (windowThread != null && windowThread != self) {
LOG.warn("Swapchain acquired on '{}' but the SDL window was created on '{}'; SDL requires acquire on the window-creating thread", self.getName(), windowThread.getName());
} else {
LOG.info("First swapchain acquire on thread={}", self.getName());
}
}
checkAcquireOnWindowThread();
final long window = device.getClaimedWindow();
if (window == 0) return false;
@@ -1,7 +1,5 @@
package com.gtnewhorizons.angelica.sdlgpu.frame;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
@@ -16,17 +14,13 @@ public final class Presenter {
this.windowThreadExecutor = windowThreadExecutor;
}
public boolean isEngaged() {
return windowThreadExecutor != null && GLStateManager.isSplashComplete();
}
public void requestPresent(long srcTexture, int srcW, int srcH) {
public void requestPresent(long srcTexture, int srcW, int srcH, int flipMode) {
pending.acquireUninterruptibly();
boolean submitted = false;
try {
windowThreadExecutor.execute(() -> {
try {
frameManager.presentOnWindowThread(srcTexture, srcW, srcH);
frameManager.presentOnWindowThread(srcTexture, srcW, srcH, flipMode);
} finally {
pending.release();
}
@@ -57,7 +57,6 @@ final class SdlFramePlots {
private static final long PLOT_PIPELINE_KEY_RECOMPUTES = Tracy.plotHandle("sdl.pipelineKeyRecomputes");
private static final long PLOT_PIPELINE_FAST_PATH_HITS = Tracy.plotHandle("sdl.pipelineFastPathHits");
private static final long PLOT_MID_FRAME_SUBMITS = Tracy.plotHandle("sdl.midFrameSubmits");
private static final long PLOT_GATE_US = Tracy.plotHandle("sdl.gateUs");
private static final int[] contextCounters = new int[SDLGPURenderBackend.CTR_COUNT];
@@ -110,6 +109,5 @@ final class SdlFramePlots {
Tracy.plotInt(PLOT_EMPTY_FRAMES, f.emptyFramesThisFrame);
Tracy.plotInt(PLOT_DROPPED_DRAWS, f.droppedDrawsThisFrame);
Tracy.plotInt(PLOT_MID_FRAME_SUBMITS, f.midFrameSubmitsThisFrame);
Tracy.plotInt(PLOT_GATE_US, f.lastFrameGateNanos / 1000);
}
}
@@ -681,7 +681,7 @@ public final class ShaderManager {
return null;
}
LOG.info("Vertex attribute conversion: program {} converted {}", program, attribs);
if (SystemProperties.DUMP_SHADERS) dumpVariantSource(program, src);
if (SystemProperties.dumpShaders()) dumpVariantSource(program, src);
return v;
} finally {
memFree(r.spirv());
@@ -80,7 +80,7 @@ public final class DxbcCrossCompile {
return new Output(copy, cached.entrypoint);
}
final int dumpId = SystemProperties.DUMP_SHADERS ? CrossCompileUtil.SHADER_DUMP_COUNTER.getAndIncrement() : -1;
final int dumpId = SystemProperties.dumpShaders() ? CrossCompileUtil.SHADER_DUMP_COUNTER.getAndIncrement() : -1;
if (dumpId >= 0) CrossCompileUtil.dumpSpirv(spirv, dumpId, glShaderType);
final String hlsl = compileToHlsl(spirv, glShaderType);
@@ -87,7 +87,7 @@ public final class MslCrossCompile {
return new Output(copy, cached.entrypoint);
}
final int dumpId = SystemProperties.DUMP_SHADERS ? CrossCompileUtil.SHADER_DUMP_COUNTER.getAndIncrement() : -1;
final int dumpId = SystemProperties.dumpShaders() ? CrossCompileUtil.SHADER_DUMP_COUNTER.getAndIncrement() : -1;
if (dumpId >= 0) CrossCompileUtil.dumpSpirv(spirv, dumpId, glShaderType);
try (MemoryStack stack = stackPush()) {
@@ -33,10 +33,10 @@ public final class SplashDispatcher {
return (long) w << 32 | (h & 0xffffffffL);
}
public static void tryDispatch(SDLGPURenderBackend backend, OffscreenTarget target) {
public static void tryDispatch(SDLGPURenderBackend backend, OffscreenTarget target, boolean force) {
if (!frameReady || target == null) return;
final long now = System.nanoTime();
if (now - lastBlitNanos < MIN_INTERVAL_NS) return;
if (!force && now - lastBlitNanos < MIN_INTERVAL_NS) return;
lastBlitNanos = now;
frameReady = false;
backend.dispatchSplashBlit(target);
@@ -10,6 +10,7 @@ import java.util.concurrent.Executor;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_NONE;
@Timeout(10)
class PresenterTest {
@@ -43,10 +44,10 @@ class PresenterTest {
final DeferredExecutor executor = new DeferredExecutor();
final Presenter p = presenter(executor);
p.requestPresent(1L, 10, 10);
p.requestPresent(1L, 10, 10, SDL_FLIP_NONE);
assertEquals(1, executor.tasks.size());
final Thread second = new Thread(() -> p.requestPresent(2L, 10, 10));
final Thread second = new Thread(() -> p.requestPresent(2L, 10, 10, SDL_FLIP_NONE));
second.start();
awaitWaiting(second);
assertEquals(1, executor.tasks.size(), "second present must not enqueue while the first is in flight");
@@ -61,7 +62,7 @@ class PresenterTest {
final DeferredExecutor executor = new DeferredExecutor();
final Presenter p = presenter(executor);
p.requestPresent(1L, 10, 10);
p.requestPresent(1L, 10, 10, SDL_FLIP_NONE);
final Thread drainer = new Thread(p::drain);
drainer.start();
@@ -74,8 +75,8 @@ class PresenterTest {
@Test
void anInlineExecutorReleasesTheSlotImmediately() {
final Presenter p = new Presenter(new FrameManager(new Device()), Runnable::run);
p.requestPresent(1L, 10, 10);
p.requestPresent(2L, 10, 10);
p.requestPresent(1L, 10, 10, SDL_FLIP_NONE);
p.requestPresent(2L, 10, 10, SDL_FLIP_NONE);
p.drain();
}
@@ -87,7 +88,7 @@ class PresenterTest {
fm.frame();
final int before = fm.registeredFrameCount();
p.requestPresent(1L, 10, 10);
p.requestPresent(1L, 10, 10, SDL_FLIP_NONE);
final Thread windowThread = new Thread(executor::runNext, "window-thread");
windowThread.start();
@@ -95,4 +96,18 @@ class PresenterTest {
assertEquals(before, fm.registeredFrameCount(), "the window thread must stay out of the frame registry");
}
@Test
void theSplashPresentGoesThroughTheWindowThreadToo() {
final DeferredExecutor executor = new DeferredExecutor();
final FrameManager fm = new FrameManager(new Device());
fm.setPresenter(new Presenter(fm, executor));
fm.frame();
final int before = fm.registeredFrameCount();
fm.presentSplash(1L, 10, 10);
assertEquals(1, executor.tasks.size(), "the splash blit must be enqueued on the window thread");
assertEquals(before, fm.registeredFrameCount(), "the splash present must not touch the calling thread's frame");
}
}
@@ -0,0 +1,76 @@
package com.gtnewhorizons.angelica.sdlgpu.shader;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
import com.gtnewhorizons.angelica.glsm.testutil.TestPaths;
import com.gtnewhorizons.angelica.sdlgpu.sampler.SamplerLookup;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL20;
import org.lwjgl.util.shaderc.Shaderc;
import java.nio.ByteBuffer;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.system.MemoryUtil.memFree;
class FontFilterVertexSamplerTest {
private static final String FONT_FILTER_VSH = TestPaths.readString("src/main/resources/assets/angelica/shaders/fontFilter.vsh");
private static String runSdlPipeline(String raw) {
final GlslVulkanPreprocess.Result pre = GlslVulkanPreprocess.run(raw, GL20.GL_VERTEX_SHADER, "fontFilter.vsh", true);
String src = pre != null ? pre.rewrittenSource() : raw;
src = ClipZRemap.injectGLToVulkanClipZ(src);
src = SamplerStripper.stripUnused(src);
return src;
}
private static ShaderManager.StageReflection reflect(String source) {
final SpirvCompiler.Result r = SpirvCompiler.compile(source, Shaderc.shaderc_vertex_shader,
"fontFilter.vsh", SpirvCompiler.Options.vulkanForced460Core());
assertNotNull(r.spirv(), () -> "fontFilter.vsh failed to compile for Vulkan: " + r.error());
final ByteBuffer spirv = r.spirv();
try {
return ShaderManager.reflectStage(spirv, true);
} finally {
memFree(spirv);
}
}
@Test
void samplerSurvivesStrippingAndReflectsOnTheVertexStage() {
final String transformed = runSdlPipeline(FONT_FILTER_VSH);
assertTrue(transformed.contains("uniform sampler2D lightmap"), "a reference inside a runtime if must keep the declaration; only preprocessor #if hides it");
final List<String> samplers = reflect(transformed).samplerNames();
assertEquals(List.of("lightmap"), samplers, "the vertex stage must expose exactly the lightmap sampler");
}
@Test
void looseVec3UniformReflectsWithAStd140Offset() {
final List<ShaderManager.UboMember> members = reflect(runSdlPipeline(FONT_FILTER_VSH)).uboMembers();
final ShaderManager.UboMember lightmap = members.stream()
.filter(m -> m.name().endsWith("u_Lightmap"))
.findFirst()
.orElseThrow(() -> new AssertionError("u_Lightmap missing from the default uniform block: " + members));
assertEquals(3, lightmap.vectorSize(), "u_Lightmap must reflect as a 3-wide vector");
assertEquals(1, lightmap.columns(), "u_Lightmap is a vector, not a matrix");
assertEquals(0, lightmap.arrayStride(), "a scalar member takes Std140Writer's contiguous path");
assertEquals(0, lightmap.offset() % 16, "std140 aligns a vec3 to 16 bytes");
}
@Test
void samplerNamedLightmapResolvesToTextureUnitOne() {
final List<String> samplers = reflect(runSdlPipeline(FONT_FILTER_VSH)).samplerNames();
final Object2IntOpenHashMap<String> noExplicitUnits = new Object2IntOpenHashMap<>();
noExplicitUnits.defaultReturnValue(-1);
assertEquals(1, SamplerLookup.getSamplerTextureUnit(samplers, noExplicitUnits, 0), "even without a glUniform1i the SDL fallback must put a sampler named 'lightmap' on unit 1");
}
}
@@ -4,19 +4,14 @@ import com.google.common.collect.ImmutableSet;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IndexBuffer;
import com.gtnewhorizon.gtnhlib.util.font.GlyphReplacements;
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.FORMATTING_CHAR;
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;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.config.FontConfig;
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.ffp.FFPVertexLighting;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
import com.gtnewhorizons.angelica.glsm.streaming.PersistentStreamingBuffer;
import com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader;
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor;
import com.gtnewhorizons.angelica.rendering.tesr.ModelPartBatcher;
import com.gtnewhorizons.angelica.rendering.tesr.TesrBatchRenderer;
@@ -29,20 +24,31 @@ import net.minecraft.util.MathHelper;
import net.minecraft.util.ResourceLocation;
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack.stackPush;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
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.memPutByte;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutShort;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memRealloc;
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.FORMATTING_CHAR;
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;
/**
* A batching replacement for {@code FontRenderer}
@@ -67,6 +73,8 @@ public class BatchingFontRenderer {
private final int AAStrength;
private final int alphaTestRefLocation;
private final int mvpMatrixLocation;
private final int lightmapLocation;
private final int lightmapSamplerLocation;
private final int fontShaderId;
final boolean isSGA;
@@ -111,6 +119,8 @@ public class BatchingFontRenderer {
AAStrength = GLStateManager.glGetUniformLocation(fontShaderId, "strength");
alphaTestRefLocation = GLStateManager.glGetUniformLocation(fontShaderId, "alphaTestRef");
mvpMatrixLocation = GLStateManager.glGetUniformLocation(fontShaderId, "u_MVPMatrix");
lightmapLocation = GLStateManager.glGetUniformLocation(fontShaderId, "u_Lightmap");
lightmapSamplerLocation = GLStateManager.glGetUniformLocation(fontShaderId, "lightmap");
if (ebo == null) {
ebo = new IndexBuffer();
vbo = GLStateManager.glGenBuffers();
@@ -128,6 +138,9 @@ public class BatchingFontRenderer {
// [v, v, t, t, c, c, c, c, tb, tb, tb, tb]
// v, t and tb are floats, c is bytes; 36 bytes total
private static final int VERTEX_SIZE = 36;
/** {@code OpenGlHelper.lightmapTexUnit} */
private static final int LIGHTMAP_TEX_UNIT = 1;
private static int rawCapacity = INITIAL_BATCH_SIZE * VERTEX_SIZE;
private static ByteBuffer vertexData = memAlloc(rawCapacity);
private static long vertexDataAddress = memAddress0(vertexData);
@@ -138,6 +151,7 @@ public class BatchingFontRenderer {
private static int fontVAO = 0;
private static int vbo;
private static IndexBuffer ebo;
private static int lightmapSamplerProgram;
private static final int RING_CAPACITY = 2 * 1024 * 1024;
private static PersistentStreamingBuffer streamRing;
@@ -158,11 +172,58 @@ public class BatchingFontRenderer {
private int blendSrcRGB = GL11.GL_SRC_ALPHA;
private int blendDstRGB = GL11.GL_ONE_MINUS_SRC_ALPHA;
private float lightmapU = 0.0f;
private float lightmapV = 0.0f;
private boolean lightmapActive = false;
private int lightmapTextureId = 0;
private final Vector3f lightingFactor = new Vector3f(1.0f, 1.0f, 1.0f);
private boolean lightingFactorActive = false;
private static final Vector4f scratchLightmapUv = new Vector4f();
private int applyLighting(int argb) {
if (!lightingFactorActive) return argb;
final int red = Math.max(0, Math.min(255, (int) (((argb >> 16) & 0xFF) * lightingFactor.x + 0.5f)));
final int green = Math.max(0, Math.min(255, (int) (((argb >> 8) & 0xFF) * lightingFactor.y + 0.5f)));
final int blue = Math.max(0, Math.min(255, (int) ((argb & 0xFF) * lightingFactor.z + 0.5f)));
return (argb & 0xFF000000) | (red << 16) | (green << 8) | blue;
}
private void captureLightmapState() {
final int unitBinding = GLStateManager.getTextures().getTextureUnitBindings(LIGHTMAP_TEX_UNIT).getBinding();
final boolean active = GLStateManager.getProjectionMatrix().m33() == 0.0f && unitBinding != 0;
float u = 0.0f;
float v = 0.0f;
int texture = 0;
if (active) {
scratchLightmapUv.set(GLSMConfig.lastBrightnessX, GLSMConfig.lastBrightnessY, 0.0f, 1.0f).mul(GLStateManager.getTextures().getTextureUnitMatrix(LIGHTMAP_TEX_UNIT));
u = scratchLightmapUv.x;
v = scratchLightmapUv.y;
texture = unitBinding;
}
if (active == lightmapActive && u == lightmapU && v == lightmapV && texture == lightmapTextureId) {
return;
}
sealBatchSegment();
lightmapActive = active;
lightmapU = u;
lightmapV = v;
lightmapTextureId = texture;
}
private static final class TextSegment {
final Matrix4f mvp = new Matrix4f();
BatchingFontRenderer owner;
int cmdStart;
int cmdEnd;
float lightmapU;
float lightmapV;
boolean lightmapActive;
int lightmapTexture;
}
// 16-bit EBO index range
@@ -183,7 +244,6 @@ public class BatchingFontRenderer {
private int batchSealedEnd;
private static BatchingFontRenderer arenaOwner;
private void allocateBuffers() {
populateEBO(rawCapacity / VERTEX_SIZE);
}
@@ -255,6 +315,7 @@ public class BatchingFontRenderer {
private void pushVtx(float x, float y, int rgba, float u, float v, float uMin, float uMax, float vMin, float vMax) {
final long ptr = vertexDataAddress + vertexDataPos;
rgba = applyLighting(rgba);
// v, v
memPutFloat(ptr, x);
@@ -414,6 +475,10 @@ public class BatchingFontRenderer {
segment.cmdStart = batchSealedEnd;
segment.cmdEnd = end;
resolveMvp(segment.mvp);
segment.lightmapActive = lightmapActive;
segment.lightmapU = lightmapU;
segment.lightmapV = lightmapV;
segment.lightmapTexture = lightmapTextureId;
batchSegments.add(segment);
batchSealedEnd = end;
}
@@ -485,6 +550,7 @@ public class BatchingFontRenderer {
final boolean isTextureEnabledBefore = GLStateManager.glIsEnabled(GL11.GL_TEXTURE_2D);
final boolean isBlendEnabledBefore = GLStateManager.glIsEnabled(GL11.GL_BLEND);
final int boundTextureBefore = GLStateManager.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
final int lightmapBindingBefore = boundLightmapTexture();
final boolean depthTestBefore = GLStateManager.getDepthTest().isEnabled();
final boolean depthMaskBefore = GLStateManager.getDepthState().isEnabled();
@@ -493,6 +559,7 @@ public class BatchingFontRenderer {
GLStateManager.glDepthMask(false);
flushLastTexture = DUMMY_RESOURCE_LOCATION;
flushTextureChanged = false;
resetFlushLightmap();
try (MemoryStack stack = stackPush()) {
final FloatBuffer mvpBuf = stack.mallocFloat(16);
for (int i = 0, n = deferredSegments.size(); i < n; i++) {
@@ -500,10 +567,12 @@ public class BatchingFontRenderer {
mvpBuf.clear();
segment.mvp.get(mvpBuf);
GLStateManager.glUniformMatrix4(segment.owner.mvpMatrixLocation, false, mvpBuf);
uploadLightmap(segment.owner.lightmapLocation, segment.lightmapActive, segment.lightmapU,
segment.lightmapV, segment.lightmapTexture);
drawCommands(segment.owner.batchCommands.elements(), segment.cmdStart, segment.cmdEnd, segment.owner);
}
}
restoreFontDrawState(prevProgram, isTextureEnabledBefore, isBlendEnabledBefore, boundTextureBefore);
restoreFontDrawState(prevProgram, isTextureEnabledBefore, isBlendEnabledBefore, boundTextureBefore, lightmapBindingBefore);
restoreDepth(depthTestBefore, depthMaskBefore);
discardDeferredText();
@@ -517,6 +586,7 @@ public class BatchingFontRenderer {
segment.owner.recycleBatchSegments();
segment.owner.batchSealedEnd = 0;
segment.owner.batchMatrixValid = false;
segment.owner.resetLightmapState();
segment.owner = null;
}
deferredSegmentPool.addAll(deferredSegments);
@@ -578,12 +648,14 @@ public class BatchingFontRenderer {
final boolean isTextureEnabledBefore = GLStateManager.glIsEnabled(GL11.GL_TEXTURE_2D);
final boolean isBlendEnabledBefore = GLStateManager.glIsEnabled(GL11.GL_BLEND);
final int boundTextureBefore = GLStateManager.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
final int lightmapBindingBefore = boundLightmapTexture();
final boolean depthTestBefore = GLStateManager.getDepthTest().isEnabled();
final boolean depthMaskBefore = GLStateManager.getDepthState().isEnabled();
setupFontDrawState();
flushLastTexture = DUMMY_RESOURCE_LOCATION;
flushTextureChanged = false;
resetFlushLightmap();
try (MemoryStack stack = stackPush()) {
final FloatBuffer mvpBuf = stack.mallocFloat(16);
for (int i = 0; i < segmentCount; i++) {
@@ -591,6 +663,7 @@ public class BatchingFontRenderer {
mvpBuf.clear();
segment.mvp.get(mvpBuf);
GLStateManager.glUniformMatrix4(mvpMatrixLocation, false, mvpBuf);
uploadLightmap(lightmapLocation, segment.lightmapActive, segment.lightmapU, segment.lightmapV, segment.lightmapTexture);
drawCommands(cmds, segment.cmdStart, segment.cmdEnd, this);
}
if (batchSealedEnd < cmdCount) {
@@ -598,10 +671,11 @@ public class BatchingFontRenderer {
resolveMvp(scratchMvp);
scratchMvp.get(mvpBuf);
GLStateManager.glUniformMatrix4(mvpMatrixLocation, false, mvpBuf);
uploadLightmap(lightmapLocation, lightmapActive, lightmapU, lightmapV, lightmapTextureId);
drawCommands(cmds, batchSealedEnd, cmdCount, this);
}
}
restoreFontDrawState(prevProgram, isTextureEnabledBefore, isBlendEnabledBefore, boundTextureBefore);
restoreFontDrawState(prevProgram, isTextureEnabledBefore, isBlendEnabledBefore, boundTextureBefore, lightmapBindingBefore);
restoreDepth(depthTestBefore, depthMaskBefore);
truncateBatchToWatermark();
@@ -624,6 +698,10 @@ public class BatchingFontRenderer {
GLStateManager.glUniform1f(AAStrength, FontConfig.fontAAStrength / 120.f);
}
GLStateManager.glUniform1f(alphaTestRefLocation, GLStateManager.getAlphaState().getReference());
if (lightmapSamplerProgram != fontShaderId) {
GLStateManager.glUniform1i(lightmapSamplerLocation, LIGHTMAP_TEX_UNIT);
lightmapSamplerProgram = fontShaderId;
}
if (fontVAO == 0) {
fontVAO = GLStateManager.glGenVertexArrays();
@@ -654,6 +732,38 @@ public class BatchingFontRenderer {
private static ResourceLocation flushLastTexture;
private static boolean flushTextureChanged;
private static float flushLastLightmapU;
private static float flushLastLightmapV;
private static boolean flushLastLightmapActive;
private static void resetFlushLightmap() {
flushLastLightmapU = Float.NaN;
flushLastLightmapV = Float.NaN;
flushLastLightmapActive = false;
}
private static int boundLightmapTexture() {
return GLStateManager.getTextures().getTextureUnitBindings(LIGHTMAP_TEX_UNIT).getBinding();
}
private static void bindLightmapTexture(int texture) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + LIGHTMAP_TEX_UNIT);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
}
private static void uploadLightmap(int uniformLocation, boolean active, float u, float v, int texture) {
if (u != flushLastLightmapU || v != flushLastLightmapV || active != flushLastLightmapActive) {
GLStateManager.glUniform3f(uniformLocation, u, v, active ? 1.0f : 0.0f);
flushLastLightmapU = u;
flushLastLightmapV = v;
flushLastLightmapActive = active;
}
if (active && texture != 0 && boundLightmapTexture() != texture) {
bindLightmapTexture(texture);
}
}
private static void drawCommands(FontDrawCmd[] cmdsData, int from, int to, BatchingFontRenderer owner) {
for (int i = from; i < to; i++) {
final FontDrawCmd cmd = cmdsData[i];
@@ -673,7 +783,7 @@ public class BatchingFontRenderer {
}
}
private static void restoreFontDrawState(int prevProgram, boolean textureEnabledBefore, boolean blendEnabledBefore, int boundTextureBefore) {
private static void restoreFontDrawState(int prevProgram, boolean textureEnabledBefore, boolean blendEnabledBefore, int boundTextureBefore, int lightmapBindingBefore) {
GLStateManager.glUseProgram(prevProgram);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
@@ -688,6 +798,9 @@ public class BatchingFontRenderer {
if (flushTextureChanged) {
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, boundTextureBefore);
}
if (boundLightmapTexture() != lightmapBindingBefore) {
bindLightmapTexture(lightmapBindingBefore);
}
}
private static void restoreDepth(boolean testBefore, boolean maskBefore) {
@@ -709,11 +822,19 @@ public class BatchingFontRenderer {
batchMatrixValid = false;
vertexDataPos = deferredVertexPos;
idxWriterIndex = deferredIdxPos;
resetLightmapState();
if (arenaOwner == this) {
arenaOwner = null;
}
}
private void resetLightmapState() {
lightmapActive = false;
lightmapU = 0.0f;
lightmapV = 0.0f;
lightmapTextureId = 0;
}
// === Actual text mesh generation
public static boolean charInRange(char what, char fromInclusive, char toInclusive) {
@@ -808,6 +929,8 @@ public class BatchingFontRenderer {
FontProviderMC.get(this.isSGA).locationFontTexture = this.locationFontTexture;
this.beginBatch();
this.captureLightmapState();
this.lightingFactorActive = FFPVertexLighting.modulatesVertexColor(this.lightingFactor);
float curX = anchorX;
try {
final int totalStringLength = string.length();
@@ -70,9 +70,7 @@ public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris
private static MultiPhaseParameters.Builder tesrMaterialPhases(ResourceLocation texture, TesrMaterial material) {
final MultiPhaseParameters.Builder b = MultiPhaseParameters.builder();
if (texture != null) {
b.texture(new RenderPhase.Texture(texture, false, false));
}
b.texture(texture != null ? new RenderPhase.Texture(texture, false, false) : NO_TEXTURE);
if (material.isNoCull()) {
b.cull(DISABLE_CULLING);
}
@@ -85,18 +83,14 @@ public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris
if (material.isDepthOnly()) {
b.writeMaskState(DEPTH_MASK);
}
if (material.cutoutAlpha() > 0) {
b.alpha(new RenderPhase.Alpha(material.cutoutAlpha()));
}
if (material.isDepthEqual()) {
b.depthTest(EQUAL_DEPTH_TEST);
}
b.alpha(material.cutoutAlpha() > 0 ? new RenderPhase.Alpha(material.cutoutAlpha()) : ZERO_ALPHA);
b.depthTest(material.isDepthEqual() ? EQUAL_DEPTH_TEST : LEQUAL_DEPTH_TEST);
switch (material.transparency()) {
case TRANSLUCENT -> b.transparency(TRANSLUCENT_TRANSPARENCY);
case ADDITIVE -> b.transparency(ADDITIVE_TRANSPARENCY);
case ADDITIVE_ALPHA -> b.transparency(LIGHTNING_TRANSPARENCY);
case GLINT -> b.transparency(GLINT_TRANSPARENCY);
case OPAQUE -> {}
case OPAQUE -> b.transparency(NO_TRANSPARENCY);
}
switch (material.special()) {
case GLINT -> b.shader(SPECIAL_GLINT);
@@ -119,21 +113,12 @@ public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris
GbufferPrograms::teardownSpecialRenderCondition);
public static RenderLayer tesr(ResourceLocation texture, TesrMaterial material) {
final MultiPhaseParameters.Builder b = tesrMaterialPhases(texture, material).shadeModel(SMOOTH_SHADE_MODEL).lightmap(ENABLE_LIGHTMAP);
if (material.transparency() == TesrMaterial.Transparency.TRANSLUCENT) {
b.target(TRANSLUCENT_TARGET);
}
final MultiPhaseParameters.Builder b = tesrMaterialPhases(texture, material).shadeModel(SMOOTH_SHADE_MODEL);
final boolean sortsTranslucent = material.transparency() != TesrMaterial.Transparency.OPAQUE;
return of("angelica_tesr_" + material.transparency().name().toLowerCase(Locale.ROOT),
BatchVertexFormats.POSITION_COLOR_TEXTURE_LIGHTF_NORMAL, GL11.GL_QUADS, 65536, true, sortsTranslucent, b.build(false));
}
public static RenderLayer tesrNoPass(ResourceLocation texture, TesrMaterial material) {
return of("angelica_tesr_nopass_" + material.transparency().name().toLowerCase(Locale.ROOT),
BatchVertexFormats.POSITION_COLOR_TEXTURE_LIGHTF_NORMAL, GL11.GL_QUADS, 65536, true, false,
tesrMaterialPhases(texture, material).build(false));
}
public static RenderLayer getOutline(ResourceLocation texture, RenderPhase.Cull cull) {
return of("outline", DefaultVertexFormat.POSITION_COLOR_TEXTURE, 7, 256, RenderLayer.MultiPhaseParameters.builder().texture(new RenderPhase.Texture(texture, false, false)).cull(cull).depthTest(ALWAYS_DEPTH_TEST).alpha(ONE_TENTH_ALPHA).fog(NO_FOG).target(OUTLINE_TARGET).build(RenderLayer.OutlineMode.IS_OUTLINE));
}
@@ -154,7 +139,7 @@ public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris
@Override
public TransparencyType getTransparencyType() {
return phases.transparency.getTransparencyType();
return phases.transparency == null ? TransparencyType.OPAQUE : phases.transparency.getTransparencyType();
}
private MultiPhase(String name, VertexFormat vertexFormat, int drawMode, int expectedBufferSize, boolean hasCrumbling, boolean translucent, MultiPhaseParameters phases) {
@@ -170,7 +155,7 @@ public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris
}
});
this.phases = phases;
this.affectedOutline = phases.outlineMode == RenderLayer.OutlineMode.AFFECTS_OUTLINE ? phases.texture.getId().map((arg2) -> {
this.affectedOutline = phases.outlineMode == RenderLayer.OutlineMode.AFFECTS_OUTLINE && phases.texture != null ? phases.texture.getId().map((arg2) -> {
return getOutline(arg2, phases.cull);
}) : Optional.empty();
this.hash = Objects.hash(new Object[]{super.hashCode(), phases});
@@ -187,7 +172,7 @@ public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris
@Override
public ResourceLocation getTextureId() {
return phases.texture.getId().orElse(null);
return phases.texture == null ? null : phases.texture.getId().orElse(null);
}
@Override
@@ -223,7 +208,9 @@ public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris
if (arg == arg2) {
return true;
} else {
return arg != null && arg2 != null ? Objects.equals(arg.phases, arg2.phases) : false;
return arg != null && arg2 != null && arg.name.equals(arg2.name)
&& arg.getVertexFormat() == arg2.getVertexFormat() && arg.getDrawMode() == arg2.getDrawMode()
&& Objects.equals(arg.phases, arg2.phases);
}
}
}
@@ -263,7 +250,13 @@ public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris
this.writeMaskState = writeMaskState;
this.shader = shader;
this.outlineMode = outlineMode;
this.phases = ImmutableList.of(this.texture, this.transparency, this.diffuseLighting, this.shadeModel, this.alpha, this.depthTest, this.cull, this.lightmap, this.fog, this.layering, this.target, this.texturing, this.writeMaskState, this.shader);
final ImmutableList.Builder<RenderPhase> declared = ImmutableList.builder();
for (RenderPhase phase : new RenderPhase[] { this.texture, this.transparency, this.diffuseLighting,
this.shadeModel, this.alpha, this.depthTest, this.cull, this.lightmap, this.fog, this.layering,
this.target, this.texturing, this.writeMaskState, this.shader }) {
if (phase != null) declared.add(phase);
}
this.phases = declared.build();
}
@Override
@@ -308,22 +301,7 @@ public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris
private RenderPhase.WriteMaskState writeMaskState;
private RenderPhase.Shader shader;
private Builder() {
this.texture = RenderPhase.NO_TEXTURE ;
this.transparency = RenderPhase.NO_TRANSPARENCY;
this.diffuseLighting = RenderPhase.DISABLE_DIFFUSE_LIGHTING;
this.shadeModel = RenderPhase.SHADE_MODEL;
this.alpha = RenderPhase.ZERO_ALPHA;
this.depthTest = RenderPhase.LEQUAL_DEPTH_TEST;
this.cull = RenderPhase.ENABLE_CULLING;
this.lightmap = RenderPhase.DISABLE_LIGHTMAP;
this.fog = RenderPhase.FOG;
this.layering = RenderPhase.NO_LAYERING;
this.target = RenderPhase.MAIN_TARGET;
this.texturing = RenderPhase.DEFAULT_TEXTURING;
this.writeMaskState = RenderPhase.ALL_MASK;
this.shader = RenderPhase.NO_SHADER;
}
private Builder() {}
public Builder texture(RenderPhase.Texture texture) {
this.texture = texture;
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.debug.flyby;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.rendering.FramePacer;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.TickEvent;
import net.minecraft.client.Minecraft;
@@ -57,6 +58,10 @@ public final class FlybyRunner {
private long plotTurning;
private int lastPhase = -1;
private long warmupSection;
private long runSection;
private long legSection;
private long[] frameTimesNs = NO_FRAMES;
private int frameCount;
private boolean framesTruncated;
@@ -180,9 +185,13 @@ public final class FlybyRunner {
this.buildPath();
this.freezeTimeAndWeather(mc);
this.state = this.warmupTicks > 0 ? State.WARMUP : State.RUNNING;
this.tick = 0;
if (this.state == State.RUNNING) this.beginMeasuring();
if (this.warmupTicks > 0) {
this.state = State.WARMUP;
this.warmupSection = Tracy.sectionEnter(Tracy.SECTION_BENCHMARK, "flyby warmup");
} else {
this.beginMeasuring();
}
LOGGER.info("Flyby {} starting at {} {} {} yaw {}", this.route.id(),
this.originX, this.originY, this.originZ, this.originYaw);
@@ -252,12 +261,18 @@ public final class FlybyRunner {
final int phase = (leg << 1) | turning;
if (phase != this.lastPhase) {
this.lastPhase = phase;
Tracy.message("flyby " + (turning != 0 ? "turn" : "leg") + " " + leg);
final String label = "flyby " + (turning != 0 ? "turn" : "leg") + " " + leg;
Tracy.message(label);
Tracy.sectionLeave(this.legSection);
this.legSection = Tracy.sectionEnter(Tracy.SECTION_BENCHMARK, label);
}
}
private void beginMeasuring() {
this.state = State.RUNNING;
Tracy.sectionLeave(this.warmupSection);
this.warmupSection = 0L;
if (Tracy.ENABLED) this.runSection = Tracy.sectionEnter(Tracy.SECTION_BENCHMARK, "flyby " + this.route.id());
this.plotLeg = Tracy.plotHandle("flyby.leg");
this.plotTurning = Tracy.plotHandle("flyby.turning");
this.lastPhase = -1;
@@ -265,11 +280,10 @@ public final class FlybyRunner {
this.frameCount = 0;
this.lastFrameNs = 0L;
this.runStartNs = System.nanoTime();
Tracy.message("flyby start route=" + this.route.id()
+ " length=" + this.runLength + this.route.lengthUnit()
+ " speed=" + this.route.speedOr(this.speed) + "b/t"
+ " ticks=" + this.runTicks
+ " sdlgpu=" + SystemProperties.USE_SDL_GPU);
FramePacer.beginStats();
if (Tracy.ENABLED) {
Tracy.message("flyby start route=" + this.route.id() + " length=" + this.runLength + this.route.lengthUnit() + " speed=" + this.route.speedOr(this.speed) + "b/t ticks=" + this.runTicks + " sdlgpu=" + SystemProperties.USE_SDL_GPU);
}
}
private void holdPosition(EntityClientPlayerMP player) {
@@ -364,10 +378,15 @@ public final class FlybyRunner {
this.state = State.SETTLE;
this.tick = 0;
final long elapsedNs = System.nanoTime() - this.runStartNs;
Tracy.message("flyby end route=" + this.route.id() + " frames=" + this.frameCount);
Tracy.sectionLeave(this.legSection);
this.legSection = 0L;
Tracy.sectionLeave(this.runSection);
this.runSection = 0L;
if (Tracy.ENABLED) Tracy.message("flyby end route=" + this.route.id() + " frames=" + this.frameCount);
final String summary = this.summarise(elapsedNs, player);
LOGGER.info(summary);
LOGGER.info(FramePacer.endStats());
if (mc.thePlayer != null) {
mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + summary));
}
@@ -422,6 +441,13 @@ public final class FlybyRunner {
if (this.isActive()) {
LOGGER.info("Flyby cancelled");
this.state = State.DONE;
Tracy.sectionLeave(this.warmupSection);
this.warmupSection = 0L;
Tracy.sectionLeave(this.legSection);
this.legSection = 0L;
Tracy.sectionLeave(this.runSection);
this.runSection = 0L;
FramePacer.endStats();
}
}
@@ -0,0 +1,35 @@
package com.gtnewhorizons.angelica.helpers;
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.FORMATTING_CHAR;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.EnumChatFormatting;
public final class RendererLivingEntityHelper {
private RendererLivingEntityHelper() {}
public static String getUpsideDownName(EntityLivingBase entity) {
if (entity instanceof EntityLiving living) {
return living.hasCustomNameTag() ? entity.getCommandSenderName() : "";
}
if (entity instanceof EntityPlayer player) {
return player.getHideCape() ? "" : entity.getCommandSenderName();
}
return entity.getCommandSenderName();
}
public static String stripFormattingCodes(String entityName) {
if (entityName == null || entityName.isEmpty()) {
return "";
}
return entityName.indexOf(FORMATTING_CHAR) < 0
? entityName
: EnumChatFormatting.getTextWithoutFormattingCodes(entityName);
}
}
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.loading.fml.transformers;
import com.gtnewhorizons.angelica.loading.AngelicaClientTweaker;
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
import com.gtnewhorizons.angelica.loading.shared.transformers.CeleritasBlockTransform;
import com.gtnewhorizons.angelica.loading.shared.transformers.TileEntityMarkerTransform;
import net.minecraft.launchwrapper.IClassTransformer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
@@ -12,10 +13,12 @@ import org.objectweb.asm.tree.ClassNode;
public class CeleritasBlockTransformer implements IClassTransformer {
private final CeleritasBlockTransform inner;
private final TileEntityMarkerTransform tileEntities;
private final String[] exclusions;
public CeleritasBlockTransformer() {
this.inner = new CeleritasBlockTransform(AngelicaClientTweaker.isObfEnv());
this.tileEntities = new TileEntityMarkerTransform(AngelicaClientTweaker.isObfEnv());
this.exclusions = inner.getTransformerExclusions();
}
@@ -31,14 +34,19 @@ public class CeleritasBlockTransformer implements IClassTransformer {
}
final ClassReader cr = new ClassReader(basicClass);
inner.trackBlockSubclasses(cr.getClassName(), cr.getSuperName());
final String className = cr.getClassName();
inner.trackBlockSubclasses(className, cr.getSuperName());
tileEntities.track(className, cr.getSuperName());
if (!inner.shouldTransform(basicClass)) {
return basicClass;
final int markers = tileEntities.markersFor(className, basicClass);
final byte[] marked = markers == 0 ? basicClass : tileEntities.addMarkers(basicClass, markers);
if (!inner.shouldTransform(marked)) {
return marked;
}
final ClassNode cn = new ClassNode();
cr.accept(cn, 0);
(markers == 0 ? cr : new ClassReader(marked)).accept(cn, 0);
final boolean changed = inner.transformClassNode(transformedName, cn);
if (changed) {
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
@@ -47,7 +55,7 @@ public class CeleritasBlockTransformer implements IClassTransformer {
AngelicaClassDump.dumpClass(transformedName, basicClass, bytes, this);
return bytes;
}
return basicClass;
return marked;
}
}
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.loading.rfb.transformers;
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
import com.gtnewhorizons.angelica.loading.shared.transformers.CeleritasBlockTransform;
import com.gtnewhorizons.angelica.loading.shared.transformers.TileEntityMarkerTransform;
import com.gtnewhorizons.retrofuturabootstrap.api.ClassHeaderMetadata;
import com.gtnewhorizons.retrofuturabootstrap.api.ClassNodeHandle;
import com.gtnewhorizons.retrofuturabootstrap.api.ExtensibleClassLoader;
@@ -9,15 +10,18 @@ import com.gtnewhorizons.retrofuturabootstrap.api.RfbClassTransformer;
import org.intellij.lang.annotations.Pattern;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.tree.ClassNode;
import java.util.jar.Manifest;
public class RFBCeleritasBlockTransformer implements RfbClassTransformer {
private final CeleritasBlockTransform inner;
private final TileEntityMarkerTransform tileEntities;
public RFBCeleritasBlockTransformer(boolean isObf) {
inner = new CeleritasBlockTransform(isObf);
tileEntities = new TileEntityMarkerTransform(isObf);
}
@Pattern("[a-z0-9-]+")
@@ -54,15 +58,32 @@ public class RFBCeleritasBlockTransformer implements RfbClassTransformer {
return false;
}
inner.trackBlockSubclasses(metadata.binaryThisName(), metadata.binarySuperName());
return inner.shouldTransform(classNode.getOriginalBytes());
final String thisName = metadata.binaryThisName();
final String superName = metadata.binarySuperName();
final byte[] originalBytes = classNode.getOriginalBytes();
inner.trackBlockSubclasses(thisName, superName);
if (context == RfbClassTransformer.Context.LCL_WITH_TRANSFORMS) {
tileEntities.track(thisName, superName);
if (tileEntities.markersFor(thisName, originalBytes) != 0) return true;
}
return inner.shouldTransform(originalBytes);
}
@Override
public boolean transformClassIfNeeded(@NotNull ExtensibleClassLoader classLoader,
@NotNull RfbClassTransformer.Context context, @Nullable Manifest manifest,
@NotNull String className, @NotNull ClassNodeHandle classNode) {
final boolean changed = inner.transformClassNode(className, classNode.getNode());
final ClassNode node = classNode.getNode();
boolean changed = false;
if (context == RfbClassTransformer.Context.LCL_WITH_TRANSFORMS) {
final int markers = tileEntities.markersFor(node.name, classNode.getOriginalBytes());
if (markers != 0) {
tileEntities.addMarkers(node, markers);
changed = true;
}
}
changed |= inner.transformClassNode(className, node);
if (changed) {
classNode.computeMaxs();
AngelicaClassDump.dumpRFBClass(className, classNode, this);
@@ -0,0 +1,130 @@
package com.gtnewhorizons.angelica.loading.shared.transformers;
import com.gtnewhorizon.gtnhlib.asm.ClassConstantPoolParser;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Marks TileEntity subclasses that declare getDescriptionPacket, shouldRenderInPass or getDistanceFrom with a
* corresponding interface, so runtime code can test with instanceof instead of reflection.
*/
public final class TileEntityMarkerTransform {
public static final int MARK_DESCRIPTION_PACKET = 1 << 0;
public static final int MARK_SHOULD_RENDER_IN_PASS = 1 << 1;
public static final int MARK_GET_DISTANCE_FROM = 1 << 2;
private static final String SENDS_DESCRIPTION_PACKET = "com/gtnewhorizons/angelica/mixins/interfaces/SendsDescriptionPacket";
private static final String OVERRIDES_SHOULD_RENDER_IN_PASS = "com/gtnewhorizons/angelica/mixins/interfaces/OverridesShouldRenderInPass";
private static final String OVERRIDES_GET_DISTANCE_FROM = "com/gtnewhorizons/angelica/mixins/interfaces/OverridesGetDistanceFrom";
private static final String TILE_ENTITY = "net/minecraft/tileentity/TileEntity";
private static final int SCAN_FLAGS = ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
private record Marker(int bit, String method, String desc, String iface) {}
private final Marker[] markers;
private final ClassConstantPoolParser prefilter;
private final Set<String> tileEntities = Collections.newSetFromMap(new ConcurrentHashMap<>());
public TileEntityMarkerTransform(boolean isObf) {
this.markers = new Marker[] {
new Marker(MARK_DESCRIPTION_PACKET, isObf ? "func_145844_m" : "getDescriptionPacket", "()Lnet/minecraft/network/Packet;", SENDS_DESCRIPTION_PACKET),
new Marker(MARK_SHOULD_RENDER_IN_PASS, "shouldRenderInPass", "(I)Z", OVERRIDES_SHOULD_RENDER_IN_PASS),
new Marker(MARK_GET_DISTANCE_FROM, isObf ? "func_145835_a" : "getDistanceFrom", "(DDD)D", OVERRIDES_GET_DISTANCE_FROM) };
final String[] methods = new String[markers.length];
for (int i = 0; i < markers.length; i++) methods[i] = markers[i].method();
this.prefilter = new ClassConstantPoolParser(methods);
}
private boolean isTileEntity(String className) {
return className != null && (className.startsWith(TILE_ENTITY) || tileEntities.contains(className));
}
public void track(String className, String superClassName) {
if (className != null && isTileEntity(superClassName)) tileEntities.add(className);
}
public int markersFor(String className, byte[] classBytes) {
if (TILE_ENTITY.equals(className) || !isTileEntity(className) || !prefilter.find(classBytes)) return 0;
final MarkerScanner scanner = new MarkerScanner();
new ClassReader(classBytes).accept(scanner, SCAN_FLAGS);
return scanner.declared & ~scanner.alreadyPresent;
}
public byte[] addMarkers(byte[] classBytes, int markers) {
final ClassReader cr = new ClassReader(classBytes);
final ClassWriter cw = new ClassWriter(cr, 0);
cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
@Override
public void visit(int version, int access, String name, String sig, String superName, String[] interfaces) {
final String[] added = interfacesFor(markers);
if (interfaces == null) {
super.visit(version, access, name, sig, superName, added);
return;
}
final String[] merged = Arrays.copyOf(interfaces, interfaces.length + added.length);
System.arraycopy(added, 0, merged, interfaces.length, added.length);
super.visit(version, access, name, sig, superName, merged);
}
}, 0);
return cw.toByteArray();
}
public void addMarkers(ClassNode cn, int markers) {
Collections.addAll(cn.interfaces, interfacesFor(markers));
}
private int bitOf(int access, String name, String desc) {
if ((access & Opcodes.ACC_PUBLIC) == 0 || (access & Opcodes.ACC_STATIC) != 0) return 0;
for (Marker m : markers) if (m.method().equals(name) && m.desc().equals(desc)) return m.bit();
return 0;
}
private int bitOf(String interfaceName) {
for (Marker m : markers) if (m.iface().equals(interfaceName)) return m.bit();
return 0;
}
private String[] interfacesFor(int wanted) {
final String[] names = new String[Integer.bitCount(wanted)];
int next = 0;
for (Marker m : markers) if ((wanted & m.bit()) != 0) names[next++] = m.iface();
return names;
}
private final class MarkerScanner extends ClassVisitor {
private int declared;
private int alreadyPresent;
MarkerScanner() {
super(Opcodes.ASM5);
}
@Override
public void visit(int version, int access, String name, String sig, String superName, String[] interfaces) {
if (interfaces == null) return;
for (String iface : interfaces) alreadyPresent |= bitOf(iface);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String sig, String[] exceptions) {
declared |= bitOf(access, name, desc);
return null;
}
}
}
@@ -70,6 +70,12 @@ public enum Mixins implements IMixins {
)
),
STARMINER_RENDERER_LIVING_ENTITY_OPTIMIZATION(new MixinBuilder()
.setPhase(Phase.LATE)
.addRequiredMod(TargetedMod.STARMINER)
.addClientMixins("client.starminer.MixinTransformClientHelper")
),
ANGELICA_ENTITY_OVERLAYS(new MixinBuilder()
.setPhase(Phase.EARLY)
.setApplyIf(() -> AngelicaConfig.entityOverlayFixes)
@@ -377,6 +383,11 @@ public enum Mixins implements IMixins {
, "celeritas.biome_blending.MixinBlockLiquid"
, "celeritas.threading.MixinForgeHooksClient"
, "celeritas.terrain.MixinChunk"
, "celeritas.terrain.MixinWorldClient_WorkerAccess"
, "celeritas.terrain.MixinWorld_WorkerMutationGuard"
, "celeritas.terrain.MixinTileEntity_AwaitingDescriptor"
, "celeritas.terrain.MixinNetHandlerPlayClient_DescriptorRepair"
, "celeritas.terrain.MixinWorld_AwaitingDescriptor"
, "celeritas.terrain.MixinRenderRegion"
, "celeritas.terrain.MixinSectionRenderDataStorage"
, "celeritas.terrain.MixinDefaultChunkRenderer"
@@ -24,6 +24,7 @@ public enum TargetedMod implements ITargetMod {
IC2("ic2.core.coremod.IC2core", "IC2"),
NATURA(null, "Natura"),
SECURITYCRAFT(null, "securitycraft"),
STARMINER("jp.mc.ancientred.starminer.core.SMPlugin", "modJ_StarMiner"),
SUPERNOVA("com.mitchej123.supernova.core.SupernovaCore", "supernova"),
THAUMCRAFT(null, "Thaumcraft"),
TINKERS_CONSTRUCT(null, "TConstruct"),
@@ -0,0 +1,12 @@
package com.gtnewhorizons.angelica.mixins.interfaces;
public interface AwaitingDescriptorTE {
boolean angelica$isAwaitingDescriptor();
void angelica$setAwaitingDescriptor(boolean awaiting);
int angelica$getDescriptorWaitTicks();
void angelica$setDescriptorWaitTicks(int ticks);
}
@@ -6,6 +6,4 @@ public interface ITileEntityBoundingBoxCache {
AxisAlignedBB angelica$getCachedRenderBoundingBox();
byte angelica$boundsClass();
byte angelica$passClass();
}
@@ -0,0 +1,3 @@
package com.gtnewhorizons.angelica.mixins.interfaces;
public interface OverridesGetDistanceFrom {}
@@ -0,0 +1,3 @@
package com.gtnewhorizons.angelica.mixins.interfaces;
public interface OverridesShouldRenderInPass {}
@@ -0,0 +1,3 @@
package com.gtnewhorizons.angelica.mixins.interfaces;
public interface SendsDescriptionPacket {}
@@ -0,0 +1,39 @@
package com.gtnewhorizons.angelica.profiling;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import net.minecraft.client.gui.GuiMainMenu;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.gui.GuiIngameMenu;
import java.util.HashMap;
import java.util.Map;
public final class TracyUiSections {
private static final Map<Class<?>, String> LABELS = new HashMap<>();
private static Class<?> current = TracyUiSections.class;
private static long section;
private TracyUiSections() {}
public static void poll(GuiScreen screen) {
if (!Tracy.ENABLED) return;
final Class<?> screenClass = screen == null ? null : screen.getClass();
if (screenClass == current) return;
current = screenClass;
Tracy.sectionLeave(section);
section = Tracy.sectionEnter(Tracy.SECTION_UI, label(screenClass));
}
private static String label(Class<?> screenClass) {
if (screenClass == null) return "in world";
String label = LABELS.get(screenClass);
if (label == null) {
if (screenClass == GuiMainMenu.class) label = "main menu";
else if (screenClass == GuiIngameMenu.class) label = "pause";
else label = screenClass.getSimpleName();
LABELS.put(screenClass, label);
}
return label;
}
}
@@ -55,6 +55,7 @@ import com.gtnewhorizons.angelica.dynamiclights.DynamicLights;
import com.gtnewhorizons.angelica.dynamiclights.config.EntityLightConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
import com.gtnewhorizons.angelica.iris.IrisGLSMBridge;
import com.gtnewhorizons.angelica.loading.AngelicaClientTweaker;
@@ -216,9 +217,15 @@ public final class ClientProxy extends CommonProxy {
}
}
private long worldSection;
@SubscribeEvent
public void worldLoad(WorldEvent.Load event) {
if (!event.world.isRemote) return;
if (Tracy.ENABLED) {
Tracy.sectionLeave(this.worldSection);
this.worldSection = Tracy.sectionEnter(Tracy.SECTION_WORLD, "world " + event.world.provider.getDimensionName() + " (dim " + event.world.provider.dimensionId + ")");
}
if (GLStateManager.isRunningSplash()) {
GLStateManager.setRunningSplash(false);
LOGGER.info("World loaded - Enabling GLSM Cache");
@@ -238,6 +245,8 @@ public final class ClientProxy extends CommonProxy {
@SubscribeEvent
public void onWorldUnload(WorldEvent.Unload event) {
if (!event.world.isRemote) return;
Tracy.sectionLeave(this.worldSection);
this.worldSection = 0L;
DynamicLights.get().removeAllLightSources();
GpuCulling.onWorldUnload();
}
@@ -4,6 +4,7 @@ import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.profiling.TracyBackend;
import java.util.concurrent.locks.LockSupport;
@@ -25,16 +26,21 @@ public final class FramePacer {
private static final Tracy.ZoneId Z_PACER_IDLE = Tracy.zoneId("pacerIdleWork", Tracy.COLOR_CLIENT);
private static final Tracy.ZoneId Z_PACER_SLEEP = Tracy.zoneId("pacerSleep", Tracy.COLOR_SWAP);
private static final long P_FRAME_GATE_US = Tracy.plotHandle("frameGateUs");
private static final long P_SLACK_US = Tracy.plotHandle("pacer.slackUs");
private static final long P_SPIN_US = Tracy.plotHandle("pacer.spinUs");
private static final long P_CEILING_HZ = Tracy.plotHandle("pacer.ceilingHz");
private static final long P_GATE_ACTIVE = Tracy.plotHandle("pacer.gateActive");
private static long deadline;
private static long lastFrameTime;
private static boolean warnedUncapped;
private static boolean pacedLastFrame;
private static boolean statsActive;
private static VSyncMode lastVSyncMode = VSyncMode.ON;
private static int lastCapHz;
private static int lastRefreshHz;
private static int lastCeilingHz;
private static IdleWork idleWork;
private static PacerSleeper sleeper = newSleeper();
@@ -47,7 +53,6 @@ public final class FramePacer {
private static int loggedRefreshHz;
private static int loggedCapHz;
private static boolean gateBlocking;
private static long lastSeenGateEnd;
private FramePacer() {}
@@ -60,6 +65,35 @@ public final class FramePacer {
idleWork = work;
}
public static boolean pacedLastFrame() {
return pacedLastFrame;
}
public static void beginStats() {
sleeper.resetStats();
statsActive = true;
}
public static String endStats() {
statsActive = false;
final String summary = sleeper.summary(configLine());
Tracy.message(summary);
return summary;
}
static boolean statsActive() {
return statsActive;
}
static String configLine(VSyncMode mode, int refreshHz, int capHz, int ceilingHz) {
return "cfg=[" + mode + " refresh=" + refreshHz + "Hz cap=" + capHz + " ceiling=" + ceilingHz + "Hz]";
}
private static String configLine() {
return configLine(lastVSyncMode, lastRefreshHz, lastCapHz,
pacingCeilingHz(lastVSyncMode, lastCapHz, lastRefreshHz, gateActive()));
}
public static int plausibleRefreshHz(int hz) {
return hz >= MIN_PLAUSIBLE_REFRESH_HZ && hz <= MAX_PLAUSIBLE_REFRESH_HZ ? hz : 0;
}
@@ -155,11 +189,6 @@ public final class FramePacer {
}
public static long pace(int ceilingHz, Runnable renderAheadWait) {
if (ceilingHz != lastCeilingHz) {
lastCeilingHz = ceilingHz;
deadline = 0L;
}
long now = System.nanoTime();
long phaseEnd;
@@ -167,6 +196,9 @@ public final class FramePacer {
final long thresholdNanos = gateBlockThresholdNanos(targetNanos);
long frameGateNanos = 0L;
pacedLastFrame = ceilingHz > 0;
Tracy.plotInt(P_CEILING_HZ, ceilingHz);
if (ceilingHz > 0) {
frameGateNanos = GLStateManager.lastFrameGateNanos();
final long gateEnd = GLStateManager.lastFrameGateEndNanos();
@@ -192,8 +224,11 @@ public final class FramePacer {
now = phaseEnd;
}
if (now < deadline) {
final long preSleep = now;
final long slackNanos = deadline - now;
Tracy.plotInt(P_SLACK_US, slackNanos / 1000);
final boolean slept = now < deadline;
if (slept) {
Tracy.beginZone(Z_PACER_SLEEP);
try {
now = sleeper.sleepUntil(deadline, now);
@@ -201,6 +236,10 @@ public final class FramePacer {
Tracy.endZone();
}
}
if (Tracy.ENABLED || statsActive) {
sleeper.noteFrame(slackNanos, slept ? now - deadline : 0L, slept);
Tracy.plotInt(P_SPIN_US, sleeper.lastSpinNanos / 1000);
}
} else {
deadline = 0L;
}
@@ -215,15 +254,18 @@ public final class FramePacer {
final long waitNanos = phaseEnd - now;
now = phaseEnd;
if (ceilingHz > 0 && waitNanos > thresholdNanos) deadline = now;
gateBlocking = observeGate(ceilingHz > 0 ? Math.max(frameGateNanos, waitNanos) : 0L, thresholdNanos);
final boolean gateBlocking = observeGate(ceilingHz > 0 ? Math.max(frameGateNanos, waitNanos) : 0L, thresholdNanos);
Tracy.plotInt(P_GATE_ACTIVE, gateBlocking ? 1 : 0);
GLStateManager.pumpDisplayMessages();
if (pacingStatusChanged(gateBlocking, lastVSyncMode, lastRefreshHz, lastCapHz)) {
final boolean blocking = settledBlocking;
final String line = pacingLine(lastVSyncMode, lastRefreshHz, lastCapHz, blocking);
if (vsyncNotHonoured(lastVSyncMode, blocking)) AngelicaMod.LOGGER.warn(line);
final boolean notHonoured = vsyncNotHonoured(lastVSyncMode, blocking);
if (notHonoured) AngelicaMod.LOGGER.warn(line);
else AngelicaMod.LOGGER.info(line);
Tracy.message(line, notHonoured ? TracyBackend.SEVERITY_WARNING : TracyBackend.SEVERITY_INFO);
}
final long period = lastFrameTime == 0L ? 0L : now - lastFrameTime;
@@ -5,31 +5,50 @@ import java.util.function.LongSupplier;
final class PacerSleeper {
private static final long SPIN_FLOOR_NANOS = 100_000L;
private static final long MIN_PARK_NANOS = 1_000_000L;
private static final long MAX_OVERSHOOT_NANOS = 4_000_000L;
private static final int SLOTS = 10;
static final int SLOTS = 10;
static final long MIN_SPIN_FLOOR_NANOS = 20_000L;
private static final long MAX_SPIN_FLOOR_NANOS = 500_000L;
private final LongSupplier clock;
private final LongConsumer parker;
private final boolean spinWait;
private final long[] overshoots = new long[SLOTS];
private long overshootSum;
private long spinFloorNanos = MIN_SPIN_FLOOR_NANOS;
private int offset;
long lastSpinNanos;
int lastParks;
private long frames;
private long slackSum;
private long parkTotal;
private long lateFrames;
private long lateMax;
PacerSleeper(LongSupplier clock, LongConsumer parker, boolean spinWait) {
this.clock = clock;
this.parker = parker;
this.spinWait = spinWait;
}
long spinFloorNanos() {
return spinFloorNanos;
}
long sleepUntil(long deadlineNanos, long now) {
resetLastFrame();
long spinFloor = spinFloorNanos;
while (true) {
final long remaining = deadlineNanos - now;
if (remaining <= SPIN_FLOOR_NANOS) break;
if (remaining <= spinFloor) break;
if (Thread.currentThread().isInterrupted()) return clock.getAsLong();
long request = remaining - avgOvershootNanos() - SPIN_FLOOR_NANOS;
long request = remaining - avgOvershootNanos() - spinFloor;
if (request <= 0L) {
if (remaining <= MIN_PARK_NANOS) break;
request = MIN_PARK_NANOS;
@@ -39,17 +58,51 @@ final class PacerSleeper {
parker.accept(request);
now = clock.getAsLong();
record(Math.min(Math.max(0L, now - before - request), MAX_OVERSHOOT_NANOS));
spinFloor = spinFloorNanos;
lastParks++;
}
if (spinWait) {
final long spinStart = now;
while (now < deadlineNanos) {
Thread.yield();
if (deadlineNanos - now > spinFloor) Thread.yield();
else Thread.onSpinWait();
now = clock.getAsLong();
}
lastSpinNanos = now - spinStart;
}
return now;
}
void noteFrame(long slackNanos, long wakeLateNanos, boolean slept) {
if (!slept) resetLastFrame();
frames++;
slackSum += slackNanos;
parkTotal += lastParks;
if (slackNanos < 0L && -slackNanos > lateMax) lateMax = -slackNanos;
if (slackNanos < 0L || wakeLateNanos > 0L) lateFrames++;
}
private void resetLastFrame() {
lastSpinNanos = 0L;
lastParks = 0;
}
void resetStats() {
frames = 0L;
slackSum = 0L;
parkTotal = 0L;
lateFrames = 0L;
lateMax = 0L;
}
String summary(String config) {
if (frames == 0L) return "pacer: " + config + " no paced frames";
return "pacer: " + config + " frames=" + frames + " parks=" + parkTotal + " slackUs avg=" + slackSum / frames / 1000L + " floorUs=" + spinFloorNanos / 1000L
+ " overshootUs est=" + avgOvershootNanos() / 1000L + " lateFrames=" + lateFrames + " lateMaxUs=" + lateMax / 1000L;
}
long avgOvershootNanos() {
return overshootSum / SLOTS;
}
@@ -58,5 +111,12 @@ final class PacerSleeper {
overshootSum += nanos - overshoots[offset];
overshoots[offset] = nanos;
offset = (offset + 1) % SLOTS;
long max = 0L;
for (final long o : overshoots) {
if (o > max) max = o;
}
final long jitter = max - avgOvershootNanos();
spinFloorNanos = Math.max(MIN_SPIN_FLOOR_NANOS, Math.min(MAX_SPIN_FLOOR_NANOS, jitter));
}
}
@@ -1,7 +1,7 @@
package com.gtnewhorizons.angelica.rendering;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice;
import net.minecraft.tileentity.TileEntity;
public class RenderThreadContext {
@@ -15,10 +15,13 @@ public class RenderThreadContext {
currentWorldSlice.remove();
}
public static TileEntity getSnapshotTE(int x, int y, int z) {
final WorldSlice slice = currentWorldSlice.get();
if (slice == null) return null;
return slice.getTileEntity(x, y, z);
public static WorldSlice get() {
return currentWorldSlice.get();
}
public static WorldSlice workerSlice() {
if (TessellatorManager.isOnMainThread()) return null;
return currentWorldSlice.get();
}
public static boolean hasWorldSlice() {
@@ -1,7 +1,6 @@
package com.gtnewhorizons.angelica.rendering;
import it.unimi.dsi.fastutil.objects.Reference2ByteOpenHashMap;
import net.minecraft.launchwrapper.Launch;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.AxisAlignedBB;
@@ -17,25 +16,12 @@ public final class TileEntityRenderBoundsRegistry {
public static final byte INFINITE = 2;
public static final byte DYNAMIC = 3;
public static final byte PASS0_ONLY = 1;
public static final byte PASS_OVERRIDES = 2;
private static final byte NOT_OVERRIDDEN = 1;
private static final byte OVERRIDDEN = 2;
private static final Reference2ByteOpenHashMap<Class<? extends TileEntity>> classRegistry = new Reference2ByteOpenHashMap<>();
private static final Reference2ByteOpenHashMap<Class<? extends TileEntity>> passClassRegistry = new Reference2ByteOpenHashMap<>();
private static final Reference2ByteOpenHashMap<Class<? extends TileEntity>> distanceOverrideRegistry = new Reference2ByteOpenHashMap<>();
private static final Set<String> dynamicClassNames = ConcurrentHashMap.newKeySet();
private static final String[] SHOULD_RENDER_IN_PASS_NAMES = { "shouldRenderInPass" };
private static String[] distanceFromNames;
static {
classRegistry.defaultReturnValue(UNKNOWN);
passClassRegistry.defaultReturnValue(UNKNOWN);
distanceOverrideRegistry.defaultReturnValue(UNKNOWN);
}
private TileEntityRenderBoundsRegistry() {}
@@ -73,72 +59,4 @@ public final class TileEntityRenderBoundsRegistry {
}
return result;
}
public static boolean overridesMethod(Class<?> clazz, Class<?> base, String[] candidateNames, Class<?>... params) {
for (String name : candidateNames) {
try {
return clazz.getMethod(name, params).getDeclaringClass() != base;
} catch (NoSuchMethodException ignored) {
} catch (Throwable t) {
return true;
}
}
return true;
}
private static String[] distanceFromNames() {
String[] names = distanceFromNames;
if (names == null) {
String resolved;
try {
resolved = Boolean.TRUE.equals(Launch.blackboard.get("fml.deobfuscatedEnvironment")) ? "getDistanceFrom" : "func_145835_a";
} catch (Throwable t) {
resolved = "getDistanceFrom";
}
distanceFromNames = names = new String[] { resolved };
}
return names;
}
public static byte classifyPass(TileEntity te) {
final Class<? extends TileEntity> clazz = te.getClass();
synchronized (passClassRegistry) {
final byte cached = passClassRegistry.getByte(clazz);
if (cached != UNKNOWN) return cached;
}
final byte result = overridesMethod(clazz, TileEntity.class, SHOULD_RENDER_IN_PASS_NAMES, int.class)
? PASS_OVERRIDES : PASS0_ONLY;
synchronized (passClassRegistry) {
passClassRegistry.put(clazz, result);
}
return result;
}
public static boolean overridesGetDistanceFrom(Class<? extends TileEntity> clazz) {
synchronized (distanceOverrideRegistry) {
final byte cached = distanceOverrideRegistry.getByte(clazz);
if (cached != UNKNOWN) return cached == OVERRIDDEN;
}
final boolean overrides = overridesMethod(clazz, TileEntity.class, distanceFromNames(), double.class, double.class, double.class);
synchronized (distanceOverrideRegistry) {
distanceOverrideRegistry.put(clazz, overrides ? OVERRIDDEN : NOT_OVERRIDDEN);
}
return overrides;
}
public static void clear() {
synchronized (classRegistry) {
classRegistry.clear();
}
synchronized (passClassRegistry) {
passClassRegistry.clear();
}
synchronized (distanceOverrideRegistry) {
distanceOverrideRegistry.clear();
}
}
}
@@ -0,0 +1,120 @@
package com.gtnewhorizons.angelica.rendering;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.CodeSource;
import java.util.Arrays;
import net.minecraft.block.Block;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.gtnewhorizons.angelica.mixins.interfaces.IRenderingRegistryExt;
import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler;
import cpw.mods.fml.client.registry.RenderingRegistry;
import cpw.mods.fml.common.Loader;
import cpw.mods.fml.common.ModContainer;
import cpw.mods.fml.common.registry.GameRegistry;
import cpw.mods.fml.common.registry.GameRegistry.UniqueIdentifier;
public final class WorkerWorldAccess {
private static final String REPORT_URL = "https://github.com/GTNewHorizons/angelica/issues";
private static final int INITIAL_SLOTS = 64;
private static final Logger LOGGER = LogManager.getLogger("Angelica");
private static final LoggedRenderTypeErrors READS = new LoggedRenderTypeErrors();
private static final LoggedRenderTypeErrors WRITES = new LoggedRenderTypeErrors();
private WorkerWorldAccess() {}
public static void readOutsideSlice(String worldMethod, int x, int y, int z, Block renderingBlock) {
if (!READS.shouldLog(renderingBlock)) return;
LOGGER.warn(describe(worldMethod, "read outside the chunk slice", x, y, z, renderingBlock), new Throwable());
}
public static void blockedWrite(String worldMethod, int x, int y, int z, Block renderingBlock) {
if (!WRITES.shouldLog(renderingBlock)) return;
LOGGER.error(describe(worldMethod, "ignored, it would corrupt world data", x, y, z, renderingBlock), new Throwable());
}
private static String describe(String worldMethod, String outcome, int x, int y, int z, Block block) {
final StringBuilder sb = new StringBuilder(224);
sb.append("Off-thread World.").append(worldMethod).append(' ').append(outcome).append(" at ").append(x).append(',').append(y).append(',').append(z);
if (block != null) {
sb.append(". Rendering ").append(nameOf(block));
final ISimpleBlockRenderingHandler isbrh = isbrhOf(block);
if (isbrh != null) {
sb.append(" via ").append(isbrh.getClass().getName());
final String owner = modIdOf(isbrh.getClass());
if (owner != null) sb.append(" from ").append(owner);
}
}
return sb.append(". Please report with this trace: ").append(REPORT_URL).toString();
}
private static String nameOf(Block block) {
try {
final UniqueIdentifier id = GameRegistry.findUniqueIdentifierFor(block);
if (id != null) return id.modId + ':' + id.name;
} catch (Throwable ignored) {}
return block.getClass().getName();
}
@SuppressWarnings("deprecation")
private static ISimpleBlockRenderingHandler isbrhOf(Block block) {
try {
return ((IRenderingRegistryExt) RenderingRegistry.instance()).getISBRH(block.getRenderType());
} catch (Throwable ignored) {
return null;
}
}
private static String modIdOf(Class<?> clazz) {
try {
final CodeSource source = clazz.getProtectionDomain() == null ? null : clazz.getProtectionDomain().getCodeSource();
if (source == null || source.getLocation() == null) return null;
final File jar = new File(jarFileUrl(source.getLocation()).toURI());
for (ModContainer mod : Loader.instance().getModList()) {
if (jar.equals(mod.getSource())) return mod.getModId();
}
} catch (Throwable ignored) {}
return null;
}
private static URL jarFileUrl(URL location) throws MalformedURLException {
if (!"jar".equals(location.getProtocol())) return location;
final String nested = location.getPath();
final int separator = nested.indexOf("!/");
return new URL(separator < 0 ? nested : nested.substring(0, separator));
}
private static final class LoggedRenderTypeErrors {
private volatile boolean[] logged = new boolean[INITIAL_SLOTS];
boolean shouldLog(Block block) {
final int renderType = block == null ? -1 : block.getRenderType();
final int slot = renderType < 0 ? 0 : renderType + 1;
final boolean[] current = this.logged;
return (slot >= current.length || !current[slot]) && markLogged(slot);
}
private synchronized boolean markLogged(int slot) {
boolean[] current = this.logged;
if (slot < current.length) {
if (current[slot]) return false;
current[slot] = true;
return true;
}
current = Arrays.copyOf(current, Math.max(slot + 1, current.length * 2));
current[slot] = true;
this.logged = current;
return true;
}
}
}
@@ -4,11 +4,13 @@ import com.gtnewhorizon.gtnhlib.blockpos.BlockPos;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.rendering.AngelicaRenderQueue;
import com.gtnewhorizons.angelica.rendering.StateAwareTessellator;
import com.gtnewhorizons.angelica.mixins.interfaces.OverridesGetDistanceFrom;
import com.gtnewhorizons.angelica.rendering.TileEntityRenderBoundsRegistry;
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProvider;
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder;
import com.gtnewhorizons.angelica.rendering.celeritas.iris.BlockRenderContext;
import com.gtnewhorizons.angelica.rendering.celeritas.iris.ContextAwareChunkVertexEncoder;
import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice;
import com.prupe.mcpatcher.mal.block.RenderBlocksUtils;
import it.unimi.dsi.fastutil.floats.FloatArrayList;
import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
@@ -111,6 +113,8 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
final BlockPos blockPos = new BlockPos(minX, minY, minZ);
final IBlockAccess region = getBlockAccess();
final WorldSlice reportingSlice = region instanceof WorldSlice slice ? slice : null;
if (reportingSlice != null) reportingSlice.setRenderingBlock(null);
final SmoothBiomeColorCache biomeColorCache = getBiomeColorCache();
onEnterExecute();
@@ -155,6 +159,8 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
continue;
}
if (reportingSlice != null) reportingSlice.setRenderingBlock(block);
final int meta = region.getBlockMetadata(x, y, z);
if (block.hasTileEntity(meta)) {
@@ -185,7 +191,7 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
renderData.culledBlockEntities.add(tileEntity);
AngelicaBuiltRenderSectionData.packSectionLocalBounds(culledBounds, aabb, minX, minY, minZ);
double teDistSq;
if (TileEntityRenderBoundsRegistry.overridesGetDistanceFrom(tileEntity.getClass())) {
if (tileEntity instanceof OverridesGetDistanceFrom) {
teDistSq = Double.POSITIVE_INFINITY;
} else {
try {
@@ -45,6 +45,7 @@ import com.gtnewhorizons.angelica.dynamiclights.IDynamicLightWorldRenderer;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.mixins.interfaces.ITileEntityBoundingBoxCache;
import com.gtnewhorizons.angelica.mixins.interfaces.OverridesShouldRenderInPass;
import com.gtnewhorizons.angelica.profiling.RenderClassTimings;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
import com.gtnewhorizons.angelica.rendering.RenderingState;
@@ -405,7 +406,7 @@ public class CeleritasWorldRenderer extends SimpleWorldRenderer<WorldClient, Ang
try {
for (int i = 0; i < frameTEs.size(); i++) {
final TileEntity te = frameTEs.get(i);
if (((ITileEntityBoundingBoxCache) te).angelica$passClass() == TileEntityRenderBoundsRegistry.PASS0_ONLY) {
if (!(te instanceof OverridesShouldRenderInPass)) {
dispatchTE(te, partialTicks);
count++;
continue;
@@ -104,6 +104,7 @@ public class WorldSlice implements IBlockAccessExtended, FLBlockAccess {
// The chunk origin of this slice
private ChunkSectionPos origin;
private StructureBoundingBox volume;
private Block renderingBlock;
private final SmoothBiomeColorCache biomeColorCache;
@@ -530,6 +531,18 @@ public class WorldSlice implements IBlockAccessExtended, FLBlockAccess {
return y << TABLE_BITS << TABLE_BITS | z << TABLE_BITS | x;
}
public boolean containsBlock(int x, int y, int z) {
return blockBoxContains(this.volume, x, y, z);
}
public void setRenderingBlock(Block block) {
this.renderingBlock = block;
}
public Block getRenderingBlock() {
return this.renderingBlock;
}
private static boolean blockBoxContains(StructureBoundingBox box, int x, int y, int z) {
return x >= box.minX && x <= box.maxX &&
y >= box.minY && y <= box.maxY &&
@@ -158,9 +158,8 @@ public final class TesrBatchRenderer {
boolean depthEqual;
TesrMaterial.SpecialRender special;
TesrShader shader;
boolean noPass;
LayerKey set(ResourceLocation texture, TesrMaterial.Transparency transparency, boolean noCull, boolean unlit, boolean noDepthWrite, boolean depthOnly, float cutoutAlpha, boolean depthEqual, TesrMaterial.SpecialRender special, TesrShader shader, boolean noPass) {
LayerKey set(ResourceLocation texture, TesrMaterial.Transparency transparency, boolean noCull, boolean unlit, boolean noDepthWrite, boolean depthOnly, float cutoutAlpha, boolean depthEqual, TesrMaterial.SpecialRender special, TesrShader shader) {
this.texture = texture;
this.transparency = transparency;
this.noCull = noCull;
@@ -171,12 +170,11 @@ public final class TesrBatchRenderer {
this.depthEqual = depthEqual;
this.special = special;
this.shader = shader;
this.noPass = noPass;
return this;
}
LayerKey copy() {
return new LayerKey().set(texture, transparency, noCull, unlit, noDepthWrite, depthOnly, cutoutAlpha, depthEqual, special, shader, noPass);
return new LayerKey().set(texture, transparency, noCull, unlit, noDepthWrite, depthOnly, cutoutAlpha, depthEqual, special, shader);
}
@Override
@@ -186,7 +184,7 @@ public final class TesrBatchRenderer {
&& noCull == other.noCull && unlit == other.unlit && noDepthWrite == other.noDepthWrite
&& depthOnly == other.depthOnly
&& Float.floatToIntBits(cutoutAlpha) == Float.floatToIntBits(other.cutoutAlpha)
&& depthEqual == other.depthEqual && special == other.special && shader == other.shader && noPass == other.noPass;
&& depthEqual == other.depthEqual && special == other.special && shader == other.shader;
}
@Override
@@ -201,7 +199,6 @@ public final class TesrBatchRenderer {
h = h * 31 + (depthEqual ? 1 : 0);
h = h * 31 + Objects.hashCode(special);
h = h * 31 + System.identityHashCode(shader);
h = h * 31 + (noPass ? 1 : 0);
return h;
}
}
@@ -211,37 +208,23 @@ public final class TesrBatchRenderer {
private ResourceLocation lastLayerTexture;
private TesrMaterial lastLayerMaterial;
private RenderLayer lastLayer;
private ResourceLocation lastImmediateTexture;
private TesrMaterial lastImmediateMaterial;
private RenderLayer lastImmediateLayer;
private RenderLayer layerFor(ResourceLocation texture, TesrMaterial material) {
if (texture == lastLayerTexture && material == lastLayerMaterial) {
return lastLayer;
}
final RenderLayer layer = layerLookup(texture, material, false);
final RenderLayer layer = layerLookup(texture, material);
lastLayerTexture = texture;
lastLayerMaterial = material;
lastLayer = layer;
return layer;
}
private RenderLayer noPassLayerFor(ResourceLocation texture, TesrMaterial material) {
if (texture == lastImmediateTexture && material == lastImmediateMaterial) {
return lastImmediateLayer;
}
final RenderLayer layer = layerLookup(texture, material, true);
lastImmediateTexture = texture;
lastImmediateMaterial = material;
lastImmediateLayer = layer;
return layer;
}
private RenderLayer layerLookup(ResourceLocation texture, TesrMaterial material, boolean noPass) {
final LayerKey key = scratchKey.set(texture, material.transparency(), material.isNoCull(), material.isUnlit(), material.isNoDepthWrite(), material.isDepthOnly(), material.cutoutAlpha(), material.isDepthEqual(), material.special(), material.shader(), noPass);
private RenderLayer layerLookup(ResourceLocation texture, TesrMaterial material) {
final LayerKey key = scratchKey.set(texture, material.transparency(), material.isNoCull(), material.isUnlit(), material.isNoDepthWrite(), material.isDepthOnly(), material.cutoutAlpha(), material.isDepthEqual(), material.special(), material.shader());
RenderLayer layer = layers.get(key);
if (layer == null) {
layer = noPass ? RenderLayer.tesrNoPass(texture, material) : RenderLayer.tesr(texture, material);
layer = RenderLayer.tesr(texture, material);
layers.put(key.copy(), layer);
}
return layer;
@@ -469,7 +452,7 @@ public final class TesrBatchRenderer {
final long end = VertexTransform.writeInstance(addr, format, template, identity, scratchVec, colorABGR, packedLight, null);
immediateScratch.position((int) (end - addr));
immediateScratch.flip();
final RenderLayer layer = noPassLayerFor(texture, material);
final RenderLayer layer = layerFor(texture, material);
GLStateManager.glPushAttrib(AngelicaBufferSource.SAVED_STATE_BITS);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
layer.startDrawing();
@@ -0,0 +1,53 @@
package com.gtnewhorizons.angelica.utils;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.tileentity.TileEntity;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.gtnewhorizons.angelica.mixins.interfaces.AwaitingDescriptorTE;
import com.gtnewhorizons.angelica.mixins.interfaces.SendsDescriptionPacket;
public final class AwaitingDescriptor {
private static final Logger LOGGER = LogManager.getLogger("Angelica");
private static final int WAIT_TICKS = 100;
private static final Set<Class<?>> NEVER_SENT = Collections.newSetFromMap(new ConcurrentHashMap<>());
private AwaitingDescriptor() {}
public static void begin(TileEntity te) {
if (!(te instanceof SendsDescriptionPacket) || NEVER_SENT.contains(te.getClass())) return;
final AwaitingDescriptorTE tile = (AwaitingDescriptorTE) te;
tile.angelica$setAwaitingDescriptor(true);
tile.angelica$setDescriptorWaitTicks(WAIT_TICKS);
}
public static void end(AwaitingDescriptorTE tile) {
tile.angelica$setAwaitingDescriptor(false);
tile.angelica$setDescriptorWaitTicks(0);
}
public static boolean defersUpdate(TileEntity te) {
final AwaitingDescriptorTE tile = (AwaitingDescriptorTE) te;
final int waiting = tile.angelica$getDescriptorWaitTicks();
if (waiting <= 0) return false;
tile.angelica$setDescriptorWaitTicks(waiting - 1);
if (waiting == 1) stopWaitingOn(te.getClass());
return true;
}
private static void stopWaitingOn(Class<?> cls) {
if (NEVER_SENT.add(cls)) {
LOGGER.warn("{} declares getDescriptionPacket() but never sent one", cls.getName());
}
}
}
@@ -202,7 +202,8 @@ public class ConcurrentTileEntityMap extends HashMap<ChunkPosition, TileEntity>
if (!isRenderWorkerThread()) return delegate.containsKey(key);
lock.readLock().lock();
try {
return delegate.containsKey(key);
final TileEntity te = delegate.get(key);
return te != null && !te.isInvalid();
} finally {
lock.readLock().unlock();
}
@@ -1,6 +1,7 @@
package net.coderbot.iris.pipeline;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import lombok.Getter;
import net.coderbot.iris.Iris;
import net.coderbot.iris.uniforms.SystemTimeUniforms;
@@ -19,6 +20,7 @@ public class PipelineManager {
private final Map<String, WorldRenderingPipeline> pipelinesPerDimension = new HashMap<>();
private WorldRenderingPipeline pipeline = new FixedFunctionWorldRenderingPipeline();
private String lastPreparedDimension = null;
private long shaderSection;
@Getter
private int versionCounterForSodiumShaderReload = 0;
@@ -41,6 +43,9 @@ public class PipelineManager {
Iris.logger.info("Creating pipeline for dimension '{}'", currentDimension);
pipeline = pipelineFactory.apply(currentDimension);
pipelinesPerDimension.put(currentDimension, pipeline);
if (Tracy.ENABLED) {
shaderSection = Tracy.sectionEnter(Tracy.SECTION_SHADERS, "shaders " + Iris.getCurrentPackName() + " / " + currentDimension);
}
} else {
pipeline = pipelinesPerDimension.get(currentDimension);
}
@@ -70,6 +75,8 @@ public class PipelineManager {
* @see <a href="https://github.com/IrisShaders/Iris/issues/1330">this GitHub issue</a>
*/
public void destroyPipeline() {
Tracy.sectionLeave(shaderSection);
shaderSection = 0L;
pipelinesPerDimension.forEach((dimensionName, pipeline) -> {
Iris.logger.info("Destroying pipeline for dimension '{}'", dimensionName);
resetTextureState();
@@ -6,7 +6,7 @@ import net.coderbot.iris.gl.uniform.UniformUpdateFrequency;
import org.joml.Matrix4f;
public class BuiltinReplacementUniforms {
private static final Matrix4f lightmapTextureMatrix;
public static final Matrix4f lightmapTextureMatrix;
static {
// This mimics the transformations done in LightTexture to the GL_TEXTURE matrix.
@@ -6,6 +6,8 @@ layout(location = 2) in vec4 a_Color;
layout(location = 3) in vec4 a_TexBounds;
uniform mat4 u_MVPMatrix;
uniform vec3 u_Lightmap;
uniform sampler2D lightmap;
out vec4 tB;
out vec4 color;
@@ -15,5 +17,10 @@ void main() {
gl_Position = u_MVPMatrix * vec4(a_Position, 0.0, 1.0);
texCoord = a_TexCoord0;
color = a_Color;
if (u_Lightmap.z > 0.5) {
color.rgb *= textureLod(lightmap, u_Lightmap.xy, 0.0).rgb;
}
tB = a_TexBounds;
}
@@ -81,12 +81,13 @@ public abstract class MixinMinecraft {
}
}
@Inject(method = "runGameLoop", at = @At(value = "INVOKE", target = "Ljava/lang/Thread;yield()V", remap = false))
private void angelica$limitFPS(CallbackInfo ci) {
if (AngelicaMod.proxy == null) return;
@WrapWithCondition(method = "runGameLoop", at = @At(value = "INVOKE", target = "Ljava/lang/Thread;yield()V", remap = false))
private boolean angelica$limitFPS() {
if (AngelicaMod.proxy == null) return true;
final int capHz = isFramerateLimitBelowMax() ? getLimitFramerate() : 0;
AngelicaMod.proxy.putFrametime(FramePacer.endFrame(capHz, angelica$renderAheadWait));
return !FramePacer.pacedLastFrame();
}
@Redirect(method = {"startGame", "toggleFullscreen"}, at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;setVSyncEnabled(Z)V", remap = false))
@@ -1,48 +1,33 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.optimizations;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.helpers.RendererLivingEntityHelper;
import net.minecraft.client.renderer.entity.RendererLivingEntity;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.MathHelper;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(RendererLivingEntity.class)
public abstract class MixinRendererLivingEntity {
public class MixinRendererLivingEntity {
@Shadow protected abstract float getDeathMaxRotation(EntityLivingBase entity);
/**
* @author mitchej123
* @reason The vanilla method triggers way too many allocations for no good reason
*/
@Overwrite
protected void rotateCorpse(EntityLivingBase entity, float p_77043_2_, float p_77043_3_, float partialTicks) {
GLStateManager.glRotatef(180.0F - p_77043_3_, 0.0F, 1.0F, 0.0F);
if (entity.deathTime > 0) {
float f3 = ((float)entity.deathTime + partialTicks - 1.0F) / 20.0F * 1.6F;
f3 = MathHelper.sqrt_float(f3);
if (f3 > 1.0F) {
f3 = 1.0F;
}
GLStateManager.glRotatef(f3 * this.getDeathMaxRotation(entity), 0.0F, 0.0F, 1.0F);
}
else if ((entity instanceof EntityLiving entityLiving) && entityLiving.hasCustomNameTag() || (entity instanceof EntityPlayer entityPlayer) && !entityPlayer.getHideCape()) {
final String s = EnumChatFormatting.getTextWithoutFormattingCodes(entity.getCommandSenderName());
if ((s.equals("Dinnerbone") || s.equals("Grumm"))) {
GLStateManager.glTranslatef(0.0F, entity.height + 0.1F, 0.0F);
GLStateManager.glRotatef(180.0F, 0.0F, 0.0F, 1.0F);
}
}
@Redirect(
method = "rotateCorpse",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/entity/EntityLivingBase;getCommandSenderName()Ljava/lang/String;"),
require = 0)
private static String angelica$skipUnusedEntityName(EntityLivingBase entity) {
return RendererLivingEntityHelper.getUpsideDownName(entity);
}
@Redirect(
method = "rotateCorpse",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/util/EnumChatFormatting;getTextWithoutFormattingCodes(Ljava/lang/String;)Ljava/lang/String;"),
require = 0)
private static String angelica$skipUnusedNameFormatting(String entityName) {
return RendererLivingEntityHelper.stripFormattingCodes(entityName);
}
}
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.mixins.early.angelica.tracy;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.profiling.GpuFrameLagMeter;
import com.gtnewhorizons.angelica.profiling.TracyFramePlots;
import com.gtnewhorizons.angelica.profiling.TracyUiSections;
import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@@ -20,6 +21,7 @@ public class MixinMinecraft_Tracy {
TracyFramePlots.onFrame((Minecraft) (Object) this);
GpuFrameLagMeter.onFrame();
Tracy.frameMark();
TracyUiSections.poll(((Minecraft) (Object) this).currentScreen);
Tracy.gpuCollect();
}
@@ -25,6 +25,8 @@ import com.gtnewhorizons.angelica.compat.ModStatus;
import com.gtnewhorizons.angelica.event.ChunkBiomeDataChangedEvent;
import com.gtnewhorizons.angelica.mixins.interfaces.IChunkTileEntityMapHolder;
import com.gtnewhorizons.angelica.rendering.RenderThreadContext;
import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice;
import com.gtnewhorizons.angelica.utils.AwaitingDescriptor;
import com.gtnewhorizons.angelica.utils.ConcurrentTileEntityMap;
@Mixin(Chunk.class)
@@ -60,22 +62,29 @@ public abstract class MixinChunk implements IChunkTileEntityMapHolder {
@Redirect(method = "getTileEntityUnsafe", at = @At(value = "INVOKE", target = "Ljava/util/Map;get(Ljava/lang/Object;)Ljava/lang/Object;"), remap = false)
private Object angelica$redirectGetTEUnsafe(Map<ChunkPosition, TileEntity> map, Object key) {
if (RenderThreadContext.hasWorldSlice()) {
return angelica$snapshotFirst(map, key);
}
@Redirect(method = "func_150806_e", at = @At(value = "INVOKE", target = "Ljava/util/Map;get(Ljava/lang/Object;)Ljava/lang/Object;"))
private Object angelica$redirectGetTEFunc(Map<ChunkPosition, TileEntity> map, Object key) {
return angelica$snapshotFirst(map, key);
}
@Unique
private Object angelica$snapshotFirst(Map<ChunkPosition, TileEntity> map, Object key) {
final WorldSlice slice = RenderThreadContext.get();
if (slice != null) {
final ChunkPosition pos = (ChunkPosition) key;
final TileEntity te = RenderThreadContext.getSnapshotTE((this.xPosition << 4) + pos.chunkPosX, pos.chunkPosY, (this.zPosition << 4) + pos.chunkPosZ);
final TileEntity te = slice.getTileEntity((this.xPosition << 4) + pos.chunkPosX, pos.chunkPosY, (this.zPosition << 4) + pos.chunkPosZ);
if (te != null) return te;
}
return map.get(key);
}
@Redirect(method = "func_150806_e", at = @At(value = "INVOKE", target = "Ljava/util/Map;get(Ljava/lang/Object;)Ljava/lang/Object;"))
private Object angelica$redirectGetTEFunc(Map<ChunkPosition, TileEntity> map, Object key) {
if (RenderThreadContext.hasWorldSlice()) {
final ChunkPosition pos = (ChunkPosition) key;
final TileEntity te = RenderThreadContext.getSnapshotTE((this.xPosition << 4) + pos.chunkPosX, pos.chunkPosY, (this.zPosition << 4) + pos.chunkPosZ);
if (te != null) return te;
}
return map.get(key);
@Redirect(method = "func_150806_e", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/Block;hasTileEntity(I)Z"), remap = false)
private boolean angelica$noWorkerTileEntityCreation(Block block, int meta) {
if (RenderThreadContext.hasWorldSlice()) return false;
return block.hasTileEntity(meta);
}
@Redirect(method = "func_150806_e", at = @At(value = "INVOKE", target = "Ljava/util/Map;remove(Ljava/lang/Object;)Ljava/lang/Object;"))
@@ -129,6 +138,7 @@ public abstract class MixinChunk implements IChunkTileEntityMapHolder {
te.xCoord = (this.xPosition << 4) + x;
te.yCoord = worldY;
te.zCoord = (this.zPosition << 4) + z;
AwaitingDescriptor.begin(te);
this.addTileEntity(te);
}
}
@@ -0,0 +1,27 @@
package com.gtnewhorizons.angelica.mixins.early.celeritas.terrain;
import net.minecraft.client.multiplayer.WorldClient;
import net.minecraft.client.network.NetHandlerPlayClient;
import net.minecraft.tileentity.TileEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import com.gtnewhorizons.angelica.mixins.interfaces.AwaitingDescriptorTE;
import com.gtnewhorizons.angelica.utils.AwaitingDescriptor;
@Mixin(NetHandlerPlayClient.class)
public class MixinNetHandlerPlayClient_DescriptorRepair {
@Redirect(method = "handleUpdateTileEntity",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/WorldClient;getTileEntity(III)Lnet/minecraft/tileentity/TileEntity;"))
private TileEntity angelica$remeshOnFirstDescriptor(WorldClient world, int x, int y, int z) {
final TileEntity te = world.getTileEntity(x, y, z);
if (te instanceof AwaitingDescriptorTE awaiting && awaiting.angelica$isAwaitingDescriptor()) {
AwaitingDescriptor.end(awaiting);
world.markBlockForUpdate(x, y, z);
}
return te;
}
}
@@ -0,0 +1,36 @@
package com.gtnewhorizons.angelica.mixins.early.celeritas.terrain;
import net.minecraft.tileentity.TileEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import com.gtnewhorizons.angelica.mixins.interfaces.AwaitingDescriptorTE;
@Mixin(TileEntity.class)
public abstract class MixinTileEntity_AwaitingDescriptor implements AwaitingDescriptorTE {
@Unique private boolean angelica$awaitingDescriptor;
@Unique private int angelica$descriptorWaitTicks;
@Override
public boolean angelica$isAwaitingDescriptor() {
return this.angelica$awaitingDescriptor;
}
@Override
public void angelica$setAwaitingDescriptor(boolean awaiting) {
this.angelica$awaitingDescriptor = awaiting;
}
@Override
public int angelica$getDescriptorWaitTicks() {
return this.angelica$descriptorWaitTicks;
}
@Override
public void angelica$setDescriptorWaitTicks(int ticks) {
this.angelica$descriptorWaitTicks = ticks;
}
}
@@ -0,0 +1,72 @@
package com.gtnewhorizons.angelica.mixins.early.celeritas.terrain;
import net.minecraft.block.Block;
import net.minecraft.client.multiplayer.WorldClient;
import net.minecraft.profiler.Profiler;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.world.World;
import net.minecraft.world.WorldProvider;
import net.minecraft.world.WorldSettings;
import net.minecraft.world.biome.BiomeGenBase;
import net.minecraft.world.storage.ISaveHandler;
import net.minecraftforge.common.util.ForgeDirection;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import com.gtnewhorizons.angelica.rendering.RenderThreadContext;
import com.gtnewhorizons.angelica.rendering.WorkerWorldAccess;
import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice;
@Mixin(WorldClient.class)
public abstract class MixinWorldClient_WorkerAccess extends World {
private MixinWorldClient_WorkerAccess(ISaveHandler saveHandler, String name, WorldProvider provider, WorldSettings settings, Profiler profiler) {
super(saveHandler, name, provider, settings, profiler);
}
@Unique
private static WorldSlice angelica$workerSlice(String method, int x, int y, int z) {
final WorldSlice slice = RenderThreadContext.workerSlice();
if (slice != null && !slice.containsBlock(x, y, z)) {
WorkerWorldAccess.readOutsideSlice(method, x, y, z, slice.getRenderingBlock());
}
return slice;
}
@Override
public Block getBlock(int x, int y, int z) {
final WorldSlice slice = angelica$workerSlice("getBlock", x, y, z);
return slice == null ? super.getBlock(x, y, z) : slice.getBlock(x, y, z);
}
@Override
public int getBlockMetadata(int x, int y, int z) {
final WorldSlice slice = angelica$workerSlice("getBlockMetadata", x, y, z);
return slice == null ? super.getBlockMetadata(x, y, z) : slice.getBlockMetadata(x, y, z);
}
@Override
public TileEntity getTileEntity(int x, int y, int z) {
final WorldSlice slice = angelica$workerSlice("getTileEntity", x, y, z);
return slice == null ? super.getTileEntity(x, y, z) : slice.getTileEntity(x, y, z);
}
@Override
public int getLightBrightnessForSkyBlocks(int x, int y, int z, int min) {
final WorldSlice slice = angelica$workerSlice("getLightBrightnessForSkyBlocks", x, y, z);
return slice == null ? super.getLightBrightnessForSkyBlocks(x, y, z, min) : slice.getLightBrightnessForSkyBlocks(x, y, z, min);
}
@Override
public boolean isSideSolid(int x, int y, int z, ForgeDirection side, boolean _default) {
final WorldSlice slice = angelica$workerSlice("isSideSolid", x, y, z);
return slice == null ? super.isSideSolid(x, y, z, side, _default) : slice.isSideSolid(x, y, z, side, _default);
}
@Override
public BiomeGenBase getBiomeGenForCoords(int x, int z) {
final WorldSlice slice = RenderThreadContext.workerSlice();
return slice == null ? super.getBiomeGenForCoords(x, z) : slice.getBiomeGenForCoords(x, z);
}
}
@@ -0,0 +1,21 @@
package com.gtnewhorizons.angelica.mixins.early.celeritas.terrain;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import com.gtnewhorizons.angelica.utils.AwaitingDescriptor;
@Mixin(World.class)
public class MixinWorld_AwaitingDescriptor {
@Redirect(method = "updateEntities", at = @At(value = "INVOKE", target = "Lnet/minecraft/tileentity/TileEntity;updateEntity()V"))
private void angelica$skipUntilDescriptorArrives(TileEntity te) {
if (!AwaitingDescriptor.defersUpdate(te)) {
te.updateEntity();
}
}
}
@@ -0,0 +1,25 @@
package com.gtnewhorizons.angelica.mixins.early.celeritas.terrain;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.gtnewhorizons.angelica.rendering.RenderThreadContext;
import com.gtnewhorizons.angelica.rendering.WorkerWorldAccess;
import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice;
@Mixin(World.class)
public abstract class MixinWorld_WorkerMutationGuard {
@Inject(method = "setTileEntity", at = @At("HEAD"), cancellable = true)
private void angelica$blockWorkerSetTileEntity(int x, int y, int z, TileEntity tileEntity, CallbackInfo ci) {
final WorldSlice slice = RenderThreadContext.workerSlice();
if (slice == null) return;
WorkerWorldAccess.blockedWrite("setTileEntity", x, y, z, slice.getRenderingBlock());
ci.cancel();
}
}
@@ -53,7 +53,6 @@ public abstract class MixinTileEntity implements ITileEntityBoundingBoxCache {
@Unique private Block angelica$cachedBBBlockType = null;
@Unique private AxisAlignedBB angelica$cachedRenderBB = null;
@Unique private byte angelica$boundsClassCached = 0; // 0 = unchecked, otherwise STATIC/INFINITE/DYNAMIC
@Unique private byte angelica$passClassCached = 0; // 0 = unchecked, otherwise PASS0_ONLY/PASS_OVERRIDES
/**
* @author mitchej123
@@ -122,11 +121,4 @@ public abstract class MixinTileEntity implements ITileEntityBoundingBoxCache {
return angelica$boundsClassCached;
}
@Override
public byte angelica$passClass() {
if (angelica$passClassCached == 0) {
angelica$passClassCached = TileEntityRenderBoundsRegistry.classifyPass((TileEntity) (Object) this);
}
return angelica$passClassCached;
}
}
@@ -0,0 +1,36 @@
package com.gtnewhorizons.angelica.mixins.late.client.starminer;
import com.gtnewhorizons.angelica.helpers.RendererLivingEntityHelper;
import net.minecraft.entity.EntityLivingBase;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Pseudo
@Mixin(targets = "jp.mc.ancientred.starminer.core.TransformClientHelper", remap = false)
public class MixinTransformClientHelper {
@Redirect(
method = "rotateCorpseByGravity(Lnet/minecraft/entity/EntityLivingBase;FFF)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/entity/EntityLivingBase;getCommandSenderName()Ljava/lang/String;",
remap = true),
require = 0)
private static String angelica$skipUnusedEntityName(EntityLivingBase entity) {
return RendererLivingEntityHelper.getUpsideDownName(entity);
}
@Redirect(
method = "rotateCorpseByGravity(Lnet/minecraft/entity/EntityLivingBase;FFF)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/util/EnumChatFormatting;getTextWithoutFormattingCodes(Ljava/lang/String;)Ljava/lang/String;",
remap = true),
require = 0)
private static String angelica$skipUnusedNameFormatting(String entityName) {
return RendererLivingEntityHelper.stripFormattingCodes(entityName);
}
}
@@ -0,0 +1,195 @@
package com.gtnewhorizons.angelica.client.font;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.testutil.TestPaths;
import net.coderbot.iris.uniforms.builtin.BuiltinReplacementUniforms;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
@GLCoreTest
class FontFilterLightmapGLTest {
private static final int LIGHTMAP_TEX_UNIT = 1;
private static final int LIGHTMAP_SIZE = 16;
private static final int VERTEX_SIZE = 36;
private static final float BRIGHTNESS_X = 128.0f;
private static final float BRIGHTNESS_Y = 64.0f;
private static final int LIT_TEXEL_X = 8;
private static final int LIT_TEXEL_Y = 4;
private static final int TEXEL_R = 0x40;
private static final int TEXEL_G = 0x80;
private static final int TEXEL_B = 0xC0;
private static final int VERTEX_COLOR = 0xFFFFFFFF;
private int program;
private int vao;
private int vbo;
private int lightmapTexture;
@AfterEach
void cleanup() {
GLStateManager.glUseProgram(0);
GLStateManager.glBindVertexArray(0);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + LIGHTMAP_TEX_UNIT);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
if (lightmapTexture != 0) { GL11.glDeleteTextures(lightmapTexture); lightmapTexture = 0; }
if (vbo != 0) { GLStateManager.glDeleteBuffers(vbo); vbo = 0; }
if (vao != 0) { GLStateManager.glDeleteVertexArrays(vao); vao = 0; }
if (program != 0) { GLStateManager.glDeleteProgram(program); program = 0; }
}
private static int compile(int type, String name) {
final int shader = GLStateManager.glCreateShader(type);
GLStateManager.glShaderSource(shader, TestPaths.readString("src/main/resources/assets/angelica/shaders/" + name));
GLStateManager.glCompileShader(shader);
assertEquals(GL11.GL_TRUE, GLStateManager.glGetShaderi(shader, GL20.GL_COMPILE_STATUS), () -> name + " failed to compile:\n" + GLStateManager.glGetShaderInfoLog(shader, 4096));
return shader;
}
private void buildProgram() {
final int vertex = compile(GL20.GL_VERTEX_SHADER, "fontFilter.vsh");
final int fragment = compile(GL20.GL_FRAGMENT_SHADER, "fontFilter.fsh");
program = GLStateManager.glCreateProgram();
GLStateManager.glAttachShader(program, vertex);
GLStateManager.glAttachShader(program, fragment);
GLStateManager.glLinkProgram(program);
assertEquals(GL11.GL_TRUE, GLStateManager.glGetProgrami(program, GL20.GL_LINK_STATUS), () -> "fontFilter failed to link:\n" + GLStateManager.glGetProgramInfoLog(program, 4096));
GLStateManager.glDeleteShader(vertex);
GLStateManager.glDeleteShader(fragment);
}
private void buildLightmapTexture() {
final ByteBuffer pixels = BufferUtils.createByteBuffer(LIGHTMAP_SIZE * LIGHTMAP_SIZE * 4);
for (int y = 0; y < LIGHTMAP_SIZE; y++) {
for (int x = 0; x < LIGHTMAP_SIZE; x++) {
final boolean lit = x == LIT_TEXEL_X && y == LIT_TEXEL_Y;
final int base = (y * LIGHTMAP_SIZE + x) * 4;
pixels.put(base, (byte) (lit ? TEXEL_R : 0));
pixels.put(base + 1, (byte) (lit ? TEXEL_G : 0));
pixels.put(base + 2, (byte) (lit ? TEXEL_B : 0));
pixels.put(base + 3, (byte) 0xFF);
}
}
lightmapTexture = GL11.glGenTextures();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + LIGHTMAP_TEX_UNIT);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, lightmapTexture);
GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, LIGHTMAP_SIZE, LIGHTMAP_SIZE, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
}
private void buildQuad() {
vao = GLStateManager.glGenVertexArrays();
GLStateManager.glBindVertexArray(vao);
final ByteBuffer data = BufferUtils.createByteBuffer(3 * VERTEX_SIZE);
putVertex(data, -0.9f, -0.9f);
putVertex(data, 0.9f, -0.9f);
putVertex(data, 0.0f, 0.9f);
data.flip();
vbo = GLStateManager.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
GLStateManager.glEnableVertexAttribArray(0);
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, VERTEX_SIZE, 0L);
GLStateManager.glEnableVertexAttribArray(1);
GLStateManager.glVertexAttribPointer(1, 2, GL11.GL_FLOAT, false, VERTEX_SIZE, 8L);
GLStateManager.glEnableVertexAttribArray(2);
GLStateManager.glVertexAttribPointer(2, 4, GL11.GL_UNSIGNED_BYTE, true, VERTEX_SIZE, 16L);
GLStateManager.glEnableVertexAttribArray(3);
GLStateManager.glVertexAttribPointer(3, 4, GL11.GL_FLOAT, false, VERTEX_SIZE, 20L);
}
private static void putVertex(ByteBuffer buf, float x, float y) {
final int base = buf.position();
buf.putFloat(base, x).putFloat(base + 4, y);
buf.putFloat(base + 8, 0.0f).putFloat(base + 12, 0.0f);
buf.putInt(base + 16, VERTEX_COLOR);
buf.putFloat(base + 20, 0.0f).putFloat(base + 24, 1.0f)
.putFloat(base + 28, 0.0f).putFloat(base + 32, 1.0f);
buf.position(base + VERTEX_SIZE);
}
private static void applyVanillaLightmapMatrix() {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + LIGHTMAP_TEX_UNIT);
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
GLStateManager.glLoadIdentity();
GLStateManager.glScalef(0.00390625f, 0.00390625f, 0.00390625f);
GLStateManager.glTranslatef(8.0f, 8.0f, 8.0f);
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
}
private float[] draw(boolean lightmapEnabled) {
buildProgram();
buildLightmapTexture();
buildQuad();
applyVanillaLightmapMatrix();
assertEquals(BuiltinReplacementUniforms.lightmapTextureMatrix, new Matrix4f(GLStateManager.getTextures().getTextureUnitMatrix(LIGHTMAP_TEX_UNIT)), "EntityRenderer#enableLightmap must reproduce the lightmap texture matrix Iris documents");
final Vector4f uv = new Vector4f(BRIGHTNESS_X, BRIGHTNESS_Y, 0.0f, 1.0f).mul(GLStateManager.getTextures().getTextureUnitMatrix(LIGHTMAP_TEX_UNIT));
GLStateManager.glViewport(0, 0, 800, 600);
GLStateManager.disableDepthTest();
GLStateManager.disableCull();
GLStateManager.disableBlend();
GLStateManager.glUseProgram(program);
GLStateManager.glUniform1i(GLStateManager.glGetUniformLocation(program, "sampler"), 0);
GLStateManager.glUniform1i(GLStateManager.glGetUniformLocation(program, "lightmap"), LIGHTMAP_TEX_UNIT);
GLStateManager.glUniform1i(GLStateManager.glGetUniformLocation(program, "aaMode"), 0);
GLStateManager.glUniform1f(GLStateManager.glGetUniformLocation(program, "strength"), 1.0f);
GLStateManager.glUniform1f(GLStateManager.glGetUniformLocation(program, "alphaTestRef"), 0.0f);
GLStateManager.glUniform3f(GLStateManager.glGetUniformLocation(program, "u_Lightmap"), uv.x, uv.y, lightmapEnabled ? 1.0f : 0.0f);
GLStateManager.glUniformMatrix4(GLStateManager.glGetUniformLocation(program, "u_MVPMatrix"), false, new Matrix4f().get(BufferUtils.createFloatBuffer(16)));
GLStateManager.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "fontFilter draw must not raise a GL error");
final ByteBuffer pixel = BufferUtils.createByteBuffer(4);
GL11.glReadPixels(400, 200, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixel);
return new float[] {
(pixel.get(0) & 0xFF) / 255.0f,
(pixel.get(1) & 0xFF) / 255.0f,
(pixel.get(2) & 0xFF) / 255.0f };
}
@Test
void enabledLightmapModulatesTheGlyphColor() {
final float[] actual = draw(true);
assertEquals(TEXEL_R / 255.0f, actual[0], 2.0f / 255.0f, "red must come from lightmap texel");
assertEquals(TEXEL_G / 255.0f, actual[1], 2.0f / 255.0f, "green must come from lightmap texel");
assertEquals(TEXEL_B / 255.0f, actual[2], 2.0f / 255.0f, "blue must come from lightmap texel");
}
@Test
void disabledLightmapLeavesTheGlyphColorAlone() {
final float[] actual = draw(false);
assertEquals(1.0f, actual[0], 2.0f / 255.0f, "u_Lightmap.z = 0 must leave the colour unmodulated");
assertEquals(1.0f, actual[1], 2.0f / 255.0f, "u_Lightmap.z = 0 must leave the colour unmodulated");
assertEquals(1.0f, actual[2], 2.0f / 255.0f, "u_Lightmap.z = 0 must leave the colour unmodulated");
}
}
@@ -4,6 +4,7 @@ import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -99,6 +100,14 @@ class FramePacerTest {
}
}
@Test
void aPeriodChangeKeepsThePhaseInsteadOfRestartingIt() {
final long previousDeadline = 1_000_000_000L;
final long newTarget = 1_000_000_000L / 76;
final long now = previousDeadline + 200_000L;
assertEquals(previousDeadline + newTarget, FramePacer.nextDeadline(previousDeadline, now, newTarget));
}
@Test
void fallingMoreThanTwoPeriodsBehindResynchronises() {
final long target = 16_666_666L;
@@ -131,4 +140,13 @@ class FramePacerTest {
assertNull(FramePacer.debugIndicator(VSyncMode.OFF, 144, 0, false));
assertNull(FramePacer.debugIndicator(VSyncMode.OFF, 0, 0, false));
}
@Test
void endingTheStatsPutsThemBackToSleep() {
FramePacer.beginStats();
assertTrue(FramePacer.statsActive());
FramePacer.endStats();
assertFalse(FramePacer.statsActive(), "a cancelled run must not leave the bookkeeping running forever");
}
}
@@ -1,70 +0,0 @@
package com.gtnewhorizons.angelica.rendering;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class OverridesMethodTest {
static class Base {
public double dist(double x, double y, double z) { return 0; }
public boolean pass(int p) { return p == 0; }
}
static class Plain extends Base {}
static class Direct extends Base {
@Override public double dist(double x, double y, double z) { return 1; }
}
static class Inherited extends Direct {}
interface DefaultDist {
default double dist(double x, double y, double z) { return 2; }
}
static class WithInterfaceDefault extends Base implements DefaultDist {}
private static final String[] DIST = { "dist" };
private static final String[] PASS = { "pass" };
private static final Class<?>[] DIST_PARAMS = { double.class, double.class, double.class };
@Test
void baseDeclarationIsNotAnOverride() {
assertFalse(TileEntityRenderBoundsRegistry.overridesMethod(Base.class, Base.class, DIST, DIST_PARAMS));
assertFalse(TileEntityRenderBoundsRegistry.overridesMethod(Plain.class, Base.class, DIST, DIST_PARAMS));
assertFalse(TileEntityRenderBoundsRegistry.overridesMethod(Plain.class, Base.class, PASS, int.class));
}
@Test
void directOverrideDetected() {
assertTrue(TileEntityRenderBoundsRegistry.overridesMethod(Direct.class, Base.class, DIST, DIST_PARAMS));
}
@Test
void inheritedOverrideDetected() {
assertTrue(TileEntityRenderBoundsRegistry.overridesMethod(Inherited.class, Base.class, DIST, DIST_PARAMS));
}
@Test
void classMethodBeatsInterfaceDefault() {
assertFalse(TileEntityRenderBoundsRegistry.overridesMethod(WithInterfaceDefault.class, Base.class, DIST, DIST_PARAMS));
}
@Test
void unresolvableNameTreatedAsOverriding() {
assertTrue(TileEntityRenderBoundsRegistry.overridesMethod(Plain.class, Base.class, new String[] { "func_145835_a" }, DIST_PARAMS));
}
@Test
void wrongParametersTreatedAsOverriding() {
assertTrue(TileEntityRenderBoundsRegistry.overridesMethod(Plain.class, Base.class, DIST, double.class));
}
@Test
void firstMatchingCandidateNameWins() {
assertTrue(TileEntityRenderBoundsRegistry.overridesMethod(Direct.class, Base.class, new String[] { "func_145835_a", "dist" }, DIST_PARAMS));
assertFalse(TileEntityRenderBoundsRegistry.overridesMethod(Plain.class, Base.class, new String[] { "func_145835_a", "dist" }, DIST_PARAMS));
}
}
@@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class PacerSleeperTest {
private static final long MS = 1_000_000L;
private static final long SPIN_FLOOR = 100_000L;
private static final long COLD_FLOOR = PacerSleeper.MIN_SPIN_FLOOR_NANOS;
private static final class FakeTime {
long now;
@@ -53,20 +53,20 @@ class PacerSleeperTest {
s.sleepUntil(10 * MS, 0L);
assertEquals(1, time.parks, "the request must be sized to the deadline, not to a fixed chunk");
assertEquals(10 * MS - SPIN_FLOOR, time.lastRequest, "the park should land one spin floor short of the deadline");
assertEquals(10 * MS - COLD_FLOOR, time.lastRequest, "the park should land one spin floor short of the deadline");
}
@Test
void whatIsLeftForTheSpinNeverExceedsTheFloor() {
void aWellBehavedTimerLeavesNoMoreThanTheFloorToSpin() {
final FakeTime time = new FakeTime();
time.lag = 20_000L;
final PacerSleeper s = sleeper(time);
for (int i = 0; i < 20; i++) {
final long deadline = time.now + 13 * MS;
final long floor = s.spinFloorNanos();
final long woke = s.sleepUntil(deadline, time.now);
assertTrue(deadline - woke <= SPIN_FLOOR,
"left " + (deadline - woke) + "ns to spin, floor is " + SPIN_FLOOR);
assertTrue(deadline - woke <= floor, "left " + (deadline - woke) + "ns to spin, floor is " + floor);
}
}
@@ -95,7 +95,7 @@ class PacerSleeperTest {
final long woke = s.sleepUntil(10 * MS, 0L);
assertEquals(2, time.parks, "a short park must be followed by another, not by a spin");
assertTrue(10 * MS - woke <= SPIN_FLOOR, "the second park should still land inside the floor");
assertTrue(10 * MS - woke <= s.spinFloorNanos(), "the second park should still land inside the floor");
}
@Test
@@ -171,12 +171,74 @@ class PacerSleeperTest {
assertEquals(0, time.parks);
}
@Test
void theSpinLoopFinishesTheFrameAndIsCounted() {
final long step = 10_000L;
final long[] clock = { 0L };
final PacerSleeper s = new PacerSleeper(() -> clock[0] += step, nanos -> clock[0] += nanos, true);
final long woke = s.sleepUntil(5 * MS, 0L);
assertEquals(5 * MS, woke, "the spin must carry the frame to the deadline");
assertEquals(1, s.lastParks);
assertEquals(10_000L, s.lastSpinNanos, "the spin covers what the park left short");
}
@Test
void aGapWiderThanTheFloorIsStillClosedWithoutParking() {
final long step = 10_000L;
final long overshoot = 2 * MS;
final long[] clock = { 0L };
final PacerSleeper s = new PacerSleeper(() -> clock[0] += step, nanos -> clock[0] += nanos + overshoot, true);
for (int i = 0; i < PacerSleeper.SLOTS; i++) {
s.sleepUntil(clock[0] + 13 * MS, clock[0]);
}
assertTrue(s.avgOvershootNanos() > MS, "the park estimate must exceed the remaining gap for this branch to be reached");
final long deadline = clock[0] + MS;
final long woke = s.sleepUntil(deadline, clock[0]);
assertEquals(0, s.lastParks, "a gap under one park quantum must not park again");
assertTrue(s.lastSpinNanos > 0L, "the spin loop has to close a gap far wider than the floor");
assertTrue(woke >= deadline, "woke " + woke + "ns short of deadline " + deadline);
}
@Test
void aSteadyPlatformNeedsOnlyTheMinimumSpinFloor() {
final FakeTime time = new FakeTime();
time.lag = 300_000L;
final PacerSleeper s = sleeper(time);
for (int i = 0; i < PacerSleeper.SLOTS; i++) {
s.sleepUntil(time.now + 13 * MS, time.now);
}
assertEquals(PacerSleeper.MIN_SPIN_FLOOR_NANOS, s.spinFloorNanos(), "identical overshoots are pure bias, not jitter, so the floor stays at its minimum");
}
@Test
void aJitteryPlatformWidensTheSpinFloorToCoverTheWorstPark() {
final FakeTime time = new FakeTime();
time.lag = 100_000L;
final PacerSleeper s = sleeper(time);
for (int i = 0; i < PacerSleeper.SLOTS - 1; i++) {
s.sleepUntil(time.now + 13 * MS, time.now);
}
time.lag = 500_000L;
s.sleepUntil(time.now + 13 * MS, time.now);
assertEquals(140_000L, s.avgOvershootNanos());
assertEquals(500_000L - 140_000L, s.spinFloorNanos(), "the floor must cover the spread between the worst park and the mean, not the mean itself");
}
@Test
void aDeadlineInsideTheFloorParksNotAtAll() {
final FakeTime time = new FakeTime();
final PacerSleeper s = sleeper(time);
final long woke = s.sleepUntil(SPIN_FLOOR, 0L);
final long woke = s.sleepUntil(COLD_FLOOR, 0L);
assertEquals(0, time.parks);
assertEquals(0L, woke);
@@ -18,43 +18,41 @@ class TesrLayerKeyTest {
@Test
void scratchProbeMatchesCopiedKey() {
final TesrBatchRenderer.LayerKey scratch = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.OPAQUE, true, false, false, false, 0.1f, false, NONE, SHADER, false);
final TesrBatchRenderer.LayerKey scratch = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.OPAQUE, true, false, false, false, 0.1f, false, NONE, SHADER);
final TesrBatchRenderer.LayerKey stored = scratch.copy();
assertNotSame(scratch, stored);
assertEquals(scratch, stored);
assertEquals(scratch.hashCode(), stored.hashCode());
scratch.set(TEX, TesrMaterial.Transparency.OPAQUE, true, false, false, false, 0.1f, false, NONE, SHADER, false);
scratch.set(TEX, TesrMaterial.Transparency.OPAQUE, true, false, false, false, 0.1f, false, NONE, SHADER);
assertEquals(stored, scratch, "re-set scratch still matches stored copy");
}
@Test
void fieldChangesBreakEquality() {
final TesrBatchRenderer.LayerKey base = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false);
final TesrBatchRenderer.LayerKey base = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null);
final TesrBatchRenderer.LayerKey other = base.copy();
other.set(null, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false);
other.set(null, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.TRANSLUCENT, false, false, false, false, 0f, false, NONE, null, false);
other.set(TEX, TesrMaterial.Transparency.TRANSLUCENT, false, false, false, false, 0f, false, NONE, null);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0.5f, false, NONE, null, false);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0.5f, false, NONE, null);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, true, 0f, false, NONE, null, false);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, true, 0f, false, NONE, null);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, TesrMaterial.SpecialRender.GLINT, null, false);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, TesrMaterial.SpecialRender.GLINT, null);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, SHADER, false);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, true);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, SHADER);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null);
assertEquals(base, other);
}
@Test
void nullTextureAndShaderHash() {
final TesrBatchRenderer.LayerKey key = new TesrBatchRenderer.LayerKey().set(null, null, false, false, false, false, 0f, false, null, null, false);
final TesrBatchRenderer.LayerKey key = new TesrBatchRenderer.LayerKey().set(null, null, false, false, false, false, 0f, false, null, null);
assertEquals(key, key.copy());
assertEquals(key.hashCode(), key.copy().hashCode());
}
@@ -0,0 +1,78 @@
package com.gtnewhorizons.angelica.rendering.tesr;
import com.gtnewhorizons.angelica.api.tesr.TesrMaterial;
import com.gtnewhorizons.angelica.compat.mojang.RenderLayer;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
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 TesrLayerStateTest {
@BeforeEach
void liveState() {
GLStateManager.disableFog();
GLStateManager.enableAlphaTest();
GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1f);
GLStateManager.enableDepthTest();
GLStateManager.glDepthFunc(GL11.GL_LEQUAL);
GLStateManager.glDepthMask(true);
GLStateManager.disableBlend();
GLStateManager.enableCull();
GLStateManager.enableLighting();
}
private static RenderLayer layer(TesrMaterial material) {
return RenderLayer.tesr(null, material);
}
@Test
void startDrawingLeavesUndeclaredPhasesToTheSurroundingContext() {
layer(TesrMaterial.CURRENT_STATE).startDrawing();
assertFalse(GLStateManager.getFogMode().isEnabled(), "fog");
}
@Test
void endDrawingLeavesUndeclaredPhasesToTheSurroundingContext() {
GLStateManager.enableFog();
final RenderLayer layer = layer(TesrMaterial.CURRENT_STATE);
layer.startDrawing();
layer.endDrawing();
assertTrue(GLStateManager.getFogMode().isEnabled(), "fog");
}
@Test
void declaredPhasesApply() {
layer(TesrMaterial.CURRENT_STATE).startDrawing();
assertFalse(GLStateManager.getAlphaTest().isEnabled(), "ZERO_ALPHA");
assertTrue(GLStateManager.getDepthTest().isEnabled(), "LEQUAL_DEPTH_TEST");
assertEquals(GL11.GL_LEQUAL, GLStateManager.getDepthState().getFunc());
assertFalse(GLStateManager.getBlendMode().isEnabled(), "NO_TRANSPARENCY");
assertEquals(GL11.GL_SMOOTH, GLStateManager.getShadeModelState().getValue(), "SMOOTH_SHADE_MODEL");
}
@Test
void everyMaterialToggleApplies() {
final TesrMaterial material = TesrMaterial.builder().noCull().unlit().noDepthWrite().cutout(0.5f).translucent().depthEqual().build();
layer(material).startDrawing();
assertFalse(GLStateManager.getCullState().isEnabled(), "noCull");
assertFalse(GLStateManager.getLightingState().isEnabled(), "unlit");
assertFalse(GLStateManager.getDepthState().isEnabled(), "noDepthWrite");
assertTrue(GLStateManager.getAlphaTest().isEnabled(), "cutout");
assertEquals(0.5f, GLStateManager.getAlphaState().getReference(), "cutout threshold");
assertTrue(GLStateManager.getBlendMode().isEnabled(), "translucent");
assertEquals(GL11.GL_SRC_ALPHA, GLStateManager.getBlendState().getSrcRgb());
assertEquals(GL11.GL_ONE_MINUS_SRC_ALPHA, GLStateManager.getBlendState().getDstRgb());
assertEquals(GL11.GL_EQUAL, GLStateManager.getDepthState().getFunc(), "depthEqual");
}
}
@@ -0,0 +1,91 @@
package com.gtnewhorizons.angelica.utils;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.world.ChunkPosition;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import com.gtnewhorizons.angelica.rendering.RenderThreadContext;
import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice;
class ConcurrentTileEntityMapTest {
private static final ChunkPosition POS = new ChunkPosition(1, 2, 3);
private final ConcurrentTileEntityMap map = new ConcurrentTileEntityMap();
@AfterEach
void leaveWorkerContext() {
RenderThreadContext.clear();
}
private static void enterWorkerContext() {
RenderThreadContext.set(mock(WorldSlice.class));
}
private static TileEntity tileEntity(boolean invalid) {
final TileEntity te = mock(TileEntity.class);
when(te.isInvalid()).thenReturn(invalid);
return te;
}
@Test
void containsKeyAgreesWithGetForAnInvalidTileEntityOnAWorker() {
map.put(POS, tileEntity(true));
enterWorkerContext();
assertNull(map.get(POS), "get must hide an invalid tile entity from a worker");
assertFalse(map.containsKey(POS), "containsKey must agree with get");
}
@Test
void containsKeyAgreesWithGetForAValidTileEntityOnAWorker() {
final TileEntity te = tileEntity(false);
map.put(POS, te);
enterWorkerContext();
assertSame(te, map.get(POS));
assertTrue(map.containsKey(POS));
}
@Test
void containsKeyReportsInvalidTileEntitiesOnTheClientThread() {
map.put(POS, tileEntity(true));
assertTrue(map.containsKey(POS), "the client thread owns invalidation and must still see the entry");
}
@Test
void removeFromAWorkerDefersInsteadOfMutating() {
final TileEntity te = tileEntity(false);
map.put(POS, te);
enterWorkerContext();
assertNull(map.remove(POS));
assertEquals(1, map.size(), "a worker must not mutate the map");
RenderThreadContext.clear();
assertSame(te, map.get(POS), "a valid tile entity survives a deferred invalidation");
}
@Test
void deferredInvalidationEvictsOnTheNextClientThreadWrite() {
map.put(POS, tileEntity(true));
enterWorkerContext();
map.get(POS);
RenderThreadContext.clear();
map.put(new ChunkPosition(4, 5, 6), tileEntity(false));
assertFalse(map.containsKey(POS));
}
}
@@ -4,6 +4,7 @@ import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.profiling.TracyBackend;
import com.gtnewhorizons.angelica.glsm.profiling.ZoneStack;
import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -18,12 +19,12 @@ import org.lwjgl.system.libffi.FFIType;
import org.lwjgl.system.libffi.LibFFI;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.nio.LongBuffer;
import java.util.concurrent.ConcurrentLinkedQueue;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.system.MemoryUtil.NULL;
import static org.lwjgl.system.MemoryUtil.memAddress;
import static org.lwjgl.system.MemoryUtil.memGetInt;
import static org.lwjgl.system.MemoryUtil.memPutAddress;
import static org.lwjgl.system.MemoryUtil.memPutByte;
import static org.lwjgl.system.MemoryUtil.memPutInt;
@@ -33,12 +34,16 @@ import static org.lwjgl.system.MemoryUtil.nmemAllocChecked;
public final class TracyClientBackend implements TracyBackend {
private static final Logger LOGGER = LogManager.getLogger("Tracy");
private static final int MAX_TEXT = 8192;
private static final int ACTIVE_OFFSET = 4;
private SharedLibrary lib;
private TracyLibrary sym;
private SrcLocInterner interner;
private long emptyString;
private FFICIF plotCif;
private int ctxSize;
int zoneCtxSize() {
return ctxSize;
}
@Override
public boolean init() {
@@ -52,18 +57,15 @@ public final class TracyClientBackend implements TracyBackend {
sym = TracyLibrary.resolve(lib);
if (sym == null) return false;
emptyString = nmemAllocChecked(1);
memPutByte(emptyString, (byte) 0);
final PointerBuffer plotArgTypes = MemoryUtil.memAllocPointer(2);
plotArgTypes.put(0, LibFFI.ffi_type_pointer.address()).put(1, LibFFI.ffi_type_double.address());
plotCif = FFICIF.malloc();
final int status = LibFFI.ffi_prep_cif(plotCif, LibFFI.FFI_DEFAULT_ABI, LibFFI.ffi_type_void, plotArgTypes);
if (status != LibFFI.FFI_OK) {
LOGGER.warn("Tracy: ffi_prep_cif failed: {}", status);
ctxSize = (int) JNI.invokeJ(sym.zoneCtxSize);
if (ctxSize <= 0 || Integer.bitCount(ctxSize) != 1) {
LOGGER.warn("Tracy: zone context size {} is not a power of two; slot addressing would be wrong", ctxSize);
return false;
}
emptyString = nmemAllocChecked(1);
memPutByte(emptyString, (byte) 0);
interner = new SrcLocInterner(this::allocSrcLoc, SystemProperties.TRACY_MAX_SRC_LOCS);
JNI.invokeV(sym.startupProfiler);
@@ -101,26 +103,64 @@ public final class TracyClientBackend implements TracyBackend {
return interner.dynamicSrcLoc();
}
private static final ConcurrentLinkedQueue<SlotArena> ARENAS = new ConcurrentLinkedQueue<>();
private static final class SlotArena {
private final long base;
private final int strideShift;
private int depth;
SlotArena(int stride) {
this.strideShift = Integer.numberOfTrailingZeros(stride);
this.base = nmemAllocChecked((long) stride * ZoneStack.MAX_DEPTH);
}
long top() {
return depth == ZoneStack.MAX_DEPTH ? NULL : base + ((long) depth << strideShift);
}
void unwindTo(long slot) {
final long index = (slot - base) >> strideShift;
if (index >= 0 && index < ZoneStack.MAX_DEPTH) depth = (int) index;
}
}
private final ThreadLocal<SlotArena> slots = ThreadLocal.withInitial(() -> {
final SlotArena arena = new SlotArena(ctxSize);
ARENAS.add(arena);
return arena;
});
@Override
public long beginZone(long srcLoc) {
return JNI.invokePJ(srcLoc, 1, sym.zoneBegin);
final SlotArena arena = slots.get();
final long slot = arena.top();
if (slot == NULL) return 0L;
JNI.invokePPV(srcLoc, 1, slot, sym.zoneBegin);
if (memGetInt(slot + ACTIVE_OFFSET) == 0) return 0L;
arena.depth++;
return slot;
}
@Override
public void endZone(long ctx) {
JNI.invokeJV(ctx, sym.zoneEnd);
if (ctx == 0L) return;
slots.get().unwindTo(ctx);
JNI.invokePV(ctx, sym.zoneEnd);
}
@Override
public void zoneText(long ctx, String text) {
if (ctx == 0L) return;
try (MemoryStack stack = stackPush()) {
final ByteBuffer utf8 = stack.UTF8(truncate(text), false);
JNI.invokeJPPV(ctx, memAddress(utf8), utf8.remaining(), sym.zoneText);
JNI.invokePPPV(ctx, memAddress(utf8), utf8.remaining(), sym.zoneText);
}
}
@Override
public void zoneValue(long ctx, long value) {
if (ctx == 0L) return;
JNI.invokePJV(ctx, value, sym.zoneValue);
}
@@ -153,19 +193,33 @@ public final class TracyClientBackend implements TracyBackend {
@Override
public void plot(long namePtr, double value) {
JNI.invokePJV(namePtr, Double.doubleToRawLongBits(value), sym.plot);
}
@Override
public void message(String text, int severity) {
try (MemoryStack stack = stackPush()) {
final LongBuffer arg0 = stack.longs(namePtr);
final DoubleBuffer arg1 = stack.doubles(value);
final PointerBuffer args = stack.pointers(memAddress(arg0), memAddress(arg1));
LibFFI.ffi_call(plotCif, sym.plot, null, args);
final ByteBuffer utf8 = stack.UTF8(truncate(text), false);
JNI.invokePPV(severity, 0, 0, (long) utf8.remaining(), memAddress(utf8), sym.message);
}
}
@Override
public void message(String text) {
public long sectionEnter(int category, String text) {
try (MemoryStack stack = stackPush()) {
final ByteBuffer utf8 = stack.UTF8(truncate(text), false);
JNI.invokePPV(memAddress(utf8), (long) utf8.remaining(), 0, sym.message);
return Integer.toUnsignedLong(JNI.invokePI(category, memAddress(stack.UTF8(truncate(text), true)), sym.sectionEnter));
}
}
@Override
public void sectionLeave(long id) {
JNI.invokeV((int) id, sym.sectionLeave);
}
@Override
public void sectionSetup(int category, String name) {
try (MemoryStack stack = stackPush()) {
JNI.invokePV(category, memAddress(stack.UTF8(truncate(name), true)), sym.sectionSetup);
}
}
@@ -191,15 +245,15 @@ public final class TracyClientBackend implements TracyBackend {
@Override
public void shutdown() {
JNI.invokeV(sym.shutdownProfiler);
for (SlotArena arena = ARENAS.poll(); arena != null; arena = ARENAS.poll()) {
MemoryUtil.nmemFree(arena.base);
}
}
private static String truncate(String text) {
return text.length() > MAX_TEXT ? text.substring(0, MAX_TEXT) : text;
}
/*
* GPU zones: port of TracyOpenGL.hpp GpuCtx (v0.13.1).
*/
private static final int GPU_QUERY_COUNT = 16 * 1024;
private static final byte GPU_CONTEXT_ID = 0;
private static final byte GPU_CONTEXT_TYPE_OPENGL = 1;
@@ -11,6 +11,7 @@ import static org.lwjgl.system.MemoryUtil.NULL;
final class TracyLibrary {
private static final Logger LOGGER = LogManager.getLogger("Tracy");
final long zoneCtxSize;
final long startupProfiler;
final long shutdownProfiler;
final long profilerStarted;
@@ -26,6 +27,9 @@ final class TracyLibrary {
final long message;
final long messageAppinfo;
final long setThreadName;
final long sectionEnter;
final long sectionLeave;
final long sectionSetup;
final long gpuZoneBegin;
final long gpuZoneEnd;
final long gpuTime;
@@ -43,21 +47,25 @@ final class TracyLibrary {
}
private TracyLibrary(SharedLibrary lib) {
zoneCtxSize = req(lib, "ang_zone_ctx_size");
startupProfiler = req(lib, "___tracy_startup_profiler");
shutdownProfiler = req(lib, "___tracy_shutdown_profiler");
profilerStarted = req(lib, "___tracy_profiler_started");
connected = req(lib, "___tracy_connected");
zoneBegin = req(lib, "___tracy_emit_zone_begin");
zoneEnd = req(lib, "___tracy_emit_zone_end");
zoneText = req(lib, "___tracy_emit_zone_text");
zoneValue = req(lib, "___tracy_emit_zone_value");
zoneBegin = req(lib, "ang_zone_begin");
zoneEnd = req(lib, "ang_zone_end");
zoneText = req(lib, "ang_zone_text");
zoneValue = req(lib, "ang_zone_value");
frameMark = req(lib, "___tracy_emit_frame_mark");
plot = req(lib, "___tracy_emit_plot");
plot = req(lib, "ang_plot");
plotInt = req(lib, "___tracy_emit_plot_int");
plotConfig = req(lib, "___tracy_emit_plot_config");
message = req(lib, "___tracy_emit_message");
message = req(lib, "___tracy_emit_logString");
messageAppinfo = req(lib, "___tracy_emit_message_appinfo");
setThreadName = req(lib, "___tracy_set_thread_name");
sectionEnter = req(lib, "ang_section_enter");
sectionLeave = req(lib, "ang_section_leave");
sectionSetup = req(lib, "ang_section_setup");
gpuZoneBegin = req(lib, "___tracy_emit_gpu_zone_begin");
gpuZoneEnd = req(lib, "___tracy_emit_gpu_zone_end");
gpuTime = req(lib, "___tracy_emit_gpu_time");
@@ -47,7 +47,7 @@ final class TracyNativeLoader {
case MACOS -> "libTracyClient.dylib";
case LINUX -> "libTracyClient.so";
};
final String resource = "/natives/tracy/" + platform + "/" + libName;
final String resource = "/natives/tracy/" + TRACY_VERSION + "/" + platform + "/" + libName;
final byte[] lib;
try (InputStream in = TracyNativeLoader.class.getResourceAsStream(resource)) {
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.13)
project(angelica-tracy-native CXX)
if(NOT DEFINED TRACY_SRC_DIR)
message(FATAL_ERROR "TRACY_SRC_DIR must be set to a Tracy source checkout")
endif()
add_subdirectory("${TRACY_SRC_DIR}" tracy)
target_sources(TracyClient PRIVATE "${CMAKE_CURRENT_LIST_DIR}/angelica_tracy_shim.cpp")
target_include_directories(TracyClient PRIVATE "${TRACY_SRC_DIR}/public")
@@ -0,0 +1,53 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include "tracy/TracyC.h"
#include "client/TracyProfiler.hpp"
#include "common/TracyApi.h"
static_assert(offsetof(TracyCZoneCtx, active) == 4, "TracyClientBackend.ACTIVE_OFFSET reads the active flag at a fixed offset of 4");
// Shim to cover things not reachable, or not efficiently usable, from Java; including header-inlined functions.
extern "C" {
TRACY_API uint64_t ang_zone_ctx_size(void) {
return (uint64_t)sizeof(TracyCZoneCtx);
}
TRACY_API void ang_zone_begin(const struct ___tracy_source_location_data* srcloc, int32_t active, void* out) {
*(TracyCZoneCtx*)out = ___tracy_emit_zone_begin(srcloc, active);
}
TRACY_API void ang_zone_end(const void* ctx) {
___tracy_emit_zone_end(*(const TracyCZoneCtx*)ctx);
}
TRACY_API void ang_zone_text(const void* ctx, const char* txt, uint64_t size) {
___tracy_emit_zone_text(*(const TracyCZoneCtx*)ctx, txt, (size_t)size);
}
TRACY_API void ang_zone_value(const void* ctx, uint64_t value) {
___tracy_emit_zone_value(*(const TracyCZoneCtx*)ctx, value);
}
TRACY_API void ang_plot(const char* name, int64_t valueBits) {
double value;
memcpy(&value, &valueBits, sizeof(value));
___tracy_emit_plot(name, value);
}
TRACY_API uint32_t ang_section_enter(uint32_t category, const char* txt) {
return tracy::Profiler::SectionEnter((uint16_t)category, "%s", txt);
}
TRACY_API void ang_section_leave(uint32_t id) {
tracy::Profiler::SectionLeave(id);
}
TRACY_API void ang_section_setup(uint32_t category, const char* txt) {
tracy::Profiler::SectionSetup((uint16_t)category, "%s", txt);
}
}
@@ -34,7 +34,7 @@ class TracyBindingSmokeTest {
static boolean nativePresent() {
for (String platform : new String[] {"windows-x64", "linux-x64", "macos-x64", "macos-arm64"}) {
for (String lib : new String[] {"TracyClient.dll", "libTracyClient.so", "libTracyClient.dylib"}) {
if (TracyBindingSmokeTest.class.getResource("/natives/tracy/" + platform + "/" + lib) != null) {
if (TracyBindingSmokeTest.class.getResource("/natives/tracy/" + TracyTags.TRACY_VERSION + "/" + platform + "/" + lib) != null) {
return true;
}
}
@@ -57,8 +57,26 @@ class TracyBindingSmokeTest {
return backend;
}
@Test
@Order(0)
void nativeIsBuiltWithOnDemand() throws Exception {
assertEquals(16, backend().zoneCtxSize(), "ctx size is 16 only with TRACY_ON_DEMAND");
}
@Test
@Order(1)
void zeroHandlesAreInertWhileDisconnected() throws Exception {
final TracyClientBackend b = backend();
assertEquals(0L, b.beginZone(b.internSrcLoc("disconnected", 0)), "zone must be inactive with no profiler attached");
b.endZone(0L);
b.zoneText(0L, "text");
b.zoneValue(0L, 1L);
assertEquals(0L, b.sectionEnter(1, "disconnected section"));
b.sectionLeave(0L);
}
@Test
@Order(2)
void internAndSingleZone() throws Exception {
final TracyClientBackend b = backend();
final long srcLoc = b.internSrcLoc("smoke", 0);
@@ -72,7 +90,7 @@ class TracyBindingSmokeTest {
}
@Test
@Order(2)
@Order(3)
void hammerZonesAcrossThreads() throws Exception {
final TracyClientBackend b = backend();
final int threads = 4;
@@ -108,7 +126,7 @@ class TracyBindingSmokeTest {
}
@Test
@Order(3)
@Order(4)
void plotsMessagesAndFrameMarks() throws Exception {
final TracyClientBackend b = backend();
final long plotName = b.internPlotName("smokePlot", 0);
@@ -126,7 +144,7 @@ class TracyBindingSmokeTest {
}
@Test
@Order(4)
@Order(5)
void dynamicSrcLocOverflowPath() throws Exception {
final TracyClientBackend b = backend();
final long dynamic = b.dynamicSrcLoc();
@@ -137,7 +155,7 @@ class TracyBindingSmokeTest {
}
@Test
@Order(5)
@Order(6)
void gpuEmitStructMarshalling() throws Exception {
final TracyClientBackend b = backend();
final long srcLoc = b.internSrcLoc("gpu-smoke", 0);
@@ -152,7 +170,7 @@ class TracyBindingSmokeTest {
}
@Test
@Order(6)
@Order(7)
void shutdownFlushes() throws Exception {
backend().shutdown();
}