Compare commits

..
6 Commits
Author SHA1 Message Date
Jason Mitchell 0cae2be1f6 Fix Chisel energised voidstone missing its border corners (#2254)
Fixes #2246
2026-10-12 18:48:33 -07:00
Jason Mitchell 85d0641a73 Speed up tests and run SDL-GPU device tests on CI (#2253)
* Speed up tests and run SDL-GPU device tests on CI
* Arch rules share one ASM scan instead of ArchUnit
* Tests needing an SDL GPU device fail without one instead of skipping
* Replace checkstyle's wildcard-import check with a test
* Use the runner's preinstalled Temurin JDKs
* Cache build/rfg so the Minecraft decompile chain can be skipped
* Unpack LWJGL natives before tests
* Pin the shared workflow commit
2026-10-12 13:26:28 -07:00
Jason Mitchell 46f8157087 New workflow (#2252) 2026-10-11 16:11:14 -07:00
Jason Mitchell d3dee841e8 Skip zero-size buffer uploads and readbacks on SDL-GPU (#2249) 2026-10-11 09:41:28 -07:00
Jason Mitchell 3d3d0f9e18 Angelica side fix to RC Lantern rendering. Works with or without a patched RC (#2251) 2026-10-11 09:27:53 -07:00
Jason Mitchell 23aa04361d Fix crash replaying display lists compiled during the splash screen (#2250) 2026-10-10 20:38:35 -07:00
52 changed files with 1079 additions and 292 deletions

No files matched your search

+36 -1
View File
@@ -9,7 +9,42 @@ on:
jobs:
build-and-test:
uses: GTNewHorizons/GTNH-Actions-Workflows/.github/workflows/build-and-test.yml@master
uses: GTNewHorizons/GTNH-Actions-Workflows/.github/workflows/build-and-test.yml@05e7cd7ce0ea015acda8c10c1c39fb3a20a49e60
with:
disable-spotless-pr: true
java-distribution: temurin
cache-rfg: true
secrets: inherit
sdl-gpu-tests:
runs-on: ubuntu-24.04
timeout-minutes: 20
concurrency:
group: sdl-gpu-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
steps:
- name: Install a software Vulkan driver
run: |
sudo apt-get update -y
sudo apt-get install -y xvfb mesa-vulkan-drivers
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: |
21
25
- uses: gradle/actions/setup-gradle@v5
- name: Run SDL-GPU tests
run: xvfb-run -a --server-args="-screen 0 1366x768x24" ./gradlew --build-cache --stacktrace --continue :sdl-gpu:glsmSdlTest :sdl-gpu:sdlGpuDeviceTest
- name: Check LWJGL did not extract natives to the shared temp dir
run: test ! -e "/tmp/lwjgl_$USER"
- name: Attach SDL-GPU test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: sdl-gpu-test-results
path: |
sdl-gpu/build/test-results/
sdl-gpu/build/reports/tests/
sdl-gpu/hs_err_pid*.log
+43 -2
View File
@@ -89,8 +89,11 @@ tasks.register<Copy>("copyDependencies") {
val embedOnly: Configuration by configurations
val shadowImplementation: Configuration by configurations
val dependencyClasspath: FileCollection = sourceSets.main.get().runtimeClasspath - sourceSets.main.get().output
val downgradeEmbedOnlyForTest by tasks.registering(DowngradeFiles::class) {
inputCollection = embedOnly
classpath = dependencyClasspath
outputs.dir(temporaryDir)
}
@@ -104,6 +107,7 @@ val stripModuleInfoFromShadow by tasks.registering(Jar::class) {
val downgradeShadowImplForTest by tasks.registering(DowngradeFiles::class) {
inputCollection = files(stripModuleInfoFromShadow.map { it.archiveFile })
classpath = dependencyClasspath
outputs.dir(temporaryDir)
}
@@ -118,6 +122,7 @@ val glsmTestFixtures: Configuration by configurations.creating {
val downgradeGlsmTestFixturesForTest by tasks.registering(DowngradeFiles::class) {
inputCollection = glsmTestFixtures
classpath = dependencyClasspath
outputs.dir(temporaryDir)
}
@@ -159,9 +164,25 @@ if (isMacOs && providers.gradleProperty("clientJvmVendor").isPresent) {
}
}
// Forge's asm-debug-all 5.0.3 cannot read the Java 25 class files ArchScan walks; this ASM has to resolve first.
val archScanAsm: Configuration by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
}
dependencies {
archScanAsm(libs.asm.tree.test)
archScanAsm(libs.asm.commons.test)
}
sourceSets.test {
compileClasspath = archScanAsm + compileClasspath
}
tasks.test {
useJUnitPlatform { excludeTags = setOf("gl-core") }
configureAngelicaJava8()
classpath = archScanAsm + classpath
jvmArgs("-Dceleritas.lwjglService=org.taumc.celeritas.lwjgl.HeadlessTestLWJGLService")
dependsOn(":glsm:classes", ":lwjgl3-backend:classes", ":sdl-gpu:classes")
}
@@ -169,6 +190,13 @@ tasks.test {
val downgradeRootMainClasses = tasks.named<DowngradeFiles>("downgradeMainClasses")
val downgradeRootTestClasses = tasks.named<DowngradeFiles>("downgradeTestClasses")
// JVMDowngrader only declares outputs for inputs that exist at configuration, so a clean tree has none.
for (name in listOf("main", "mixin", "test")) {
tasks.named<DowngradeFiles>("downgrade${name.replaceFirstChar { it.uppercase() }}Classes") {
sourceSets[name].output.classesDirs.files.forEach { outputs.dir(temporaryDir.resolve(it.name)) }
}
}
val glCoreTest by tasks.registering(Test::class) {
description = "Runs the gl-core tests in their own JVM with a real GL context."
group = "verification"
@@ -185,10 +213,23 @@ val glCoreTest by tasks.registering(Test::class) {
.plus(sourceSets["test"].runtimeClasspath.minus(rawMain).minus(rawTest))
configureAngelicaJava8()
mustRunAfter(tasks.test)
}
tasks.test { finalizedBy(glCoreTest) }
val verifyTestsRan by tasks.registering
listOf(tasks.test, glCoreTest).forEach { testTask ->
val name = testTask.name
val verify = tasks.register("verify${name.replaceFirstChar { it.uppercase() }}Ran") {
val resultsDir = layout.buildDirectory.dir("test-results/$name")
dependsOn(testTask)
doLast {
val dir = resultsDir.get().asFile
val xmls = dir.listFiles { f -> f.name.startsWith("TEST-") && f.name.endsWith(".xml") } ?: emptyArray()
check(xmls.isNotEmpty()) { ":$name produced no TEST-*.xml in $dir - test task likely went NO-SOURCE" }
}
}
verifyTestsRan.configure { dependsOn(verify) }
}
tasks.check { dependsOn(verifyTestsRan) }
apply(from = "gradle/angelica-shadow-common.gradle.kts")
apply(from = "experimental-surround/surround.root.gradle.kts")
+32
View File
@@ -0,0 +1,32 @@
import org.gradle.api.Project
import org.gradle.api.file.ArchiveOperations
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.Sync
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.the
import javax.inject.Inject
abstract class UnpackLwjgl3Natives : Sync() {
@get:Inject
abstract val archives: ArchiveOperations
}
// LWJGL extracts jar-packed natives to a shared temp dir with no cross-process safety; plain files it loads in place.
fun Project.registerLwjgl3NativesUnpack(): TaskProvider<UnpackLwjgl3Natives> {
val jars = the<SourceSetContainer>()["test"].runtimeClasspath.filter {
it.name.startsWith("lwjgl") && it.name.contains("-natives-") && !it.name.startsWith("lwjgl-platform-")
}
return tasks.register<UnpackLwjgl3Natives>("unpackLwjgl3Natives") {
val archives = archives
from(jars.elements.map { files -> files.map { archives.zipTree(it) } })
exclude("META-INF/**")
into(layout.buildDirectory.dir("lwjgl3-natives"))
}
}
fun Test.loadLwjgl3NativesFrom(unpack: TaskProvider<UnpackLwjgl3Natives>) {
classpath = project.files(unpack) + classpath
}
-1
View File
@@ -204,7 +204,6 @@ afterEvaluate {
testImplementation(libs.junit.platform.reporting)
testImplementation(libs.junit.platform.launcher)
testImplementation(libs.archunit.junit5)
testImplementation(libs.retrofuturabootstrap) { isTransitive = false }
glsmTestFixtures(testFixtures(project(":glsm")) as ModuleDependency) { isTransitive = false }
@@ -48,6 +48,7 @@ repositories {
}
tasks.withType<DowngradeFiles>().configureEach {
outputs.cacheIf { true }
downgradeTo.set(JavaVersion.VERSION_1_8)
multiReleaseOriginal.set(false)
multiReleaseVersions.set(emptySet())
+4
View File
@@ -183,6 +183,7 @@ dependencies {
}
tasks.withType<DowngradeFiles>().configureEach {
outputs.cacheIf { true }
downgradeTo.set(JavaVersion.VERSION_1_8)
multiReleaseOriginal.set(false)
multiReleaseVersions.set(emptySet())
@@ -302,6 +303,8 @@ val verifyTestsRan by tasks.registering {
}
tasks.check { dependsOn(verifyTestsRan) }
val unpackLwjgl3Natives = registerLwjgl3NativesUnpack()
val spirvTest by tasks.registering(Test::class) {
description = "Runs SpirvShaderTranslator* tests against native shaderc + spvc on Java 21."
group = "verification"
@@ -330,6 +333,7 @@ val spirvTest by tasks.registering(Test::class) {
"-Djava.library.path=${extractNatives3.get().property("destinationFolder").let { (it as DirectoryProperty).asFile.get().path }}",
"--enable-native-access=ALL-UNNAMED"
)
loadLwjgl3NativesFrom(unpackLwjgl3Natives)
testLogging {
showStandardStreams = true
@@ -1025,7 +1025,7 @@ public class DisplayListManager {
}
final DisplayListCommand[] complexObjects = commandBuffer.getComplexObjects();
compiled = new CompiledDisplayList(commandBuffer.finish(), complexObjects, compiledBuffers, indexedBatches);
compiled = new CompiledDisplayList(commandBuffer.finish(), complexObjects, compiledBuffers, indexedBatches, GLStateManager.ctx());
currentRecorder = null;
} catch (Exception e) {
GLStateManager.LOGGER.error("Encountered a fatal issue while trying to compile a display list.");
@@ -525,8 +525,9 @@ public final class CommandBufferExecutor {
ptr += 4;
}
case GLCommand.BIND_VAO -> {
GLStateManager.glBindVertexArray(memGetInt(ptr));
final int vao = memGetInt(ptr);
ptr += 4;
if (compiledDisplayList.replaysVertexArrayBind(vao)) GLStateManager.glBindVertexArray(vao);
}
// === Complex object reference ===
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLContextState;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
@@ -30,12 +31,13 @@ public final class CompiledDisplayList {
* Empty display list singleton. Per OpenGL spec, an empty list is still valid.
* This instance does nothing when rendered and is never deleted (shared singleton).
*/
public static final CompiledDisplayList EMPTY = new CompiledDisplayList(null, null, null, Collections.emptyList());
public static final CompiledDisplayList EMPTY = new CompiledDisplayList(null, null, null, Collections.emptyList(), null);
private final ByteBuffer commandBuffer; // Off-heap command storage, must be freed
private final DisplayListCommand[] complexObjects; // Complex commands (TexImage2D, etc.)
private final DisplayListVBO ownedVbos; // GPU resources referenced by index
private final List<IndexedDrawBatch> indexedBatches; // Shared VAO/VBO/EBO triples per layout group
private GLContextState compileCtx;
private Int2IntMap replayPrograms;
private long programLifetimeGeneration = -1;
@@ -55,11 +57,16 @@ public final class CompiledDisplayList {
return valid ? program : 0;
}
public CompiledDisplayList(ByteBuffer commandBuffer, DisplayListCommand[] complexObjects, DisplayListVBO ownedVbos, List<IndexedDrawBatch> indexedBatches) {
public CompiledDisplayList(ByteBuffer commandBuffer, DisplayListCommand[] complexObjects, DisplayListVBO ownedVbos, List<IndexedDrawBatch> indexedBatches, GLContextState compileCtx) {
this.commandBuffer = commandBuffer;
this.complexObjects = complexObjects;
this.ownedVbos = ownedVbos;
this.indexedBatches = indexedBatches;
this.compileCtx = compileCtx;
}
boolean replaysVertexArrayBind(int vao) {
return vao == 0 || compileCtx == GLStateManager.ctx();
}
/**
@@ -101,6 +108,7 @@ public final class CompiledDisplayList {
if (commandBuffer != null) {
memFree(commandBuffer);
}
compileCtx = null;
}
/**
@@ -3,21 +3,20 @@ package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
public final class BatchedIndexedDrawCmd implements DisplayListCommand {
private int sharedVAO;
private IndexedDrawBatch batch;
private int drawMode;
private int indexCount;
private int indexType;
private long indexByteOffset;
public void fill(int sharedVAO, int drawMode, int indexCount, int indexType, long indexByteOffset) {
this.sharedVAO = sharedVAO;
public void fill(IndexedDrawBatch batch, int drawMode, int indexCount, int indexType, long indexByteOffset) {
this.batch = batch;
this.drawMode = drawMode;
this.indexCount = indexCount;
this.indexType = indexType;
this.indexByteOffset = indexByteOffset;
}
public int getSharedVAO() { return sharedVAO; }
public int getDrawMode() { return drawMode; }
public int getIndexCount() { return indexCount; }
public int getIndexType() { return indexType; }
@@ -25,13 +24,13 @@ public final class BatchedIndexedDrawCmd implements DisplayListCommand {
@Override
public void execute() {
if (sharedVAO == 0) return;
GLStateManager.glBindVertexArray(sharedVAO);
if (batch == null) return;
batch.bind();
GLStateManager.glDrawElements(drawMode, indexCount, indexType, indexByteOffset);
}
@Override
public String toString() {
return "BatchedIndexedDrawCmd(vao=" + sharedVAO + ", mode=" + drawMode + ", count=" + indexCount + ", type=" + indexType + ", offset=" + indexByteOffset + ")";
return "BatchedIndexedDrawCmd(batch=" + batch + ", mode=" + drawMode + ", count=" + indexCount + ", type=" + indexType + ", offset=" + indexByteOffset + ")";
}
}
@@ -1,34 +1,60 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.GLContextState;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.lwjgl.opengl.GL15;
public final class IndexedDrawBatch {
private int sharedVAO;
private final AttribLayoutKey layout;
private int sharedVBO;
private int sharedEBO;
private int vao;
private GLContextState vaoCtx;
private boolean deleted;
public IndexedDrawBatch(int sharedVAO, int sharedVBO, int sharedEBO) {
this.sharedVAO = sharedVAO;
public IndexedDrawBatch(AttribLayoutKey layout, int sharedVBO, int sharedEBO) {
this.layout = layout;
this.sharedVBO = sharedVBO;
this.sharedEBO = sharedEBO;
}
public int getSharedVAO() { return sharedVAO; }
public int getSharedVBO() { return sharedVBO; }
public int getSharedEBO() { return sharedEBO; }
public void bind() {
final GLContextState glCtx = GLStateManager.ctx();
if (vaoCtx == glCtx) {
GLStateManager.glBindVertexArray(vao);
return;
}
final int prevVBO = GLStateManager.getBoundVBO();
final int v = GLStateManager.glGenVertexArrays();
GLStateManager.glBindVertexArray(v);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, sharedVBO);
final int stride = layout.stride();
for (int i = 0; i < layout.locations.length; i++) {
final int loc = layout.locations[i];
GLStateManager.glVertexAttribPointer(loc, layout.sizes[i], layout.types[i], layout.normalized[i], stride, layout.offset(i));
GLStateManager.glEnableVertexAttribArray(loc);
}
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEBO);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
vao = v;
vaoCtx = glCtx;
}
public void delete() {
if (deleted) return;
deleted = true;
if (sharedVAO != 0) GLStateManager.glDeleteVertexArrays(sharedVAO);
if (vaoCtx == GLStateManager.ctx()) GLStateManager.glDeleteVertexArrays(vao);
if (sharedVBO != 0) GLStateManager.glDeleteBuffers(sharedVBO);
if (sharedEBO != 0) GLStateManager.glDeleteBuffers(sharedEBO);
sharedVAO = sharedVBO = sharedEBO = 0;
vao = sharedVBO = sharedEBO = 0;
vaoCtx = null;
}
@Override
public String toString() {
return "IndexedDrawBatch(vao=" + sharedVAO + ", vbo=" + sharedVBO + ", ebo=" + sharedEBO + ")";
return "IndexedDrawBatch(vao=" + vao + ", vbo=" + sharedVBO + ", ebo=" + sharedEBO + ")";
}
}
@@ -35,7 +35,6 @@ public final class IndexedDrawBatchBuilder {
final List<IndexedDrawBatch> out = new ArrayList<>();
if (captures.isEmpty()) return out;
final int prevVAO = GLStateManager.getBoundVAO();
final int prevVBO = GLStateManager.getBoundVBO();
final int prevEBO = GLStateManager.getBoundEBO();
@@ -58,7 +57,6 @@ public final class IndexedDrawBatchBuilder {
if (!success) {
for (GroupResult r : results) r.batch.delete();
}
GLStateManager.glBindVertexArray(prevVAO);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
}
@@ -69,9 +67,8 @@ public final class IndexedDrawBatchBuilder {
private record GroupResult(IndexedDrawBatch batch, List<IndexedDrawCapture> captures,
int[] drawModes, int[] indexCounts, long[] indexOffsets) {
void commit() {
final int sharedVAO = batch.getSharedVAO();
for (int i = 0; i < captures.size(); i++) {
captures.get(i).placeholder.fill(sharedVAO, drawModes[i], indexCounts[i], GL11.GL_UNSIGNED_INT, indexOffsets[i]);
captures.get(i).placeholder.fill(batch, drawModes[i], indexCounts[i], GL11.GL_UNSIGNED_INT, indexOffsets[i]);
}
}
}
@@ -87,7 +84,6 @@ public final class IndexedDrawBatchBuilder {
throw new IllegalStateException("batch exceeds 2GiB — split needed");
}
int sharedVAO = 0;
int sharedVBO = 0;
int sharedEBO = 0;
ByteBuffer eboData = null;
@@ -99,12 +95,10 @@ public final class IndexedDrawBatchBuilder {
final long[] indexOffsets = new long[n];
try {
sharedVAO = GLStateManager.glGenVertexArrays();
sharedVBO = GLStateManager.glGenBuffers();
sharedEBO = GLStateManager.glGenBuffers();
eboData = MemoryUtilities.memAlloc((int) totalIndexBytes);
GLStateManager.glBindVertexArray(sharedVAO);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, sharedVBO);
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, totalVertexBytes, GL15.GL_STATIC_DRAW);
@@ -146,23 +140,12 @@ public final class IndexedDrawBatchBuilder {
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEBO);
GLStateManager.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, eboData, GL15.GL_STATIC_DRAW);
final AttribLayoutKey layout = group.get(0).layoutKey;
final int stride = layout.stride();
for (int i = 0; i < layout.locations.length; i++) {
final int loc = layout.locations[i];
final int size = layout.sizes[i];
final int type = layout.types[i];
GLStateManager.glVertexAttribPointer(loc, size, type, layout.normalized[i], stride, layout.offset(i));
GLStateManager.glEnableVertexAttribArray(loc);
}
success = true;
return new GroupResult(new IndexedDrawBatch(sharedVAO, sharedVBO, sharedEBO),
return new GroupResult(new IndexedDrawBatch(group.get(0).layoutKey, sharedVBO, sharedEBO),
group, drawModes, indexCounts, indexOffsets);
} finally {
if (eboData != null) MemoryUtilities.memFree(eboData);
if (!success) {
if (sharedVAO != 0) GLStateManager.glDeleteVertexArrays(sharedVAO);
if (sharedVBO != 0) GLStateManager.glDeleteBuffers(sharedVBO);
if (sharedEBO != 0) GLStateManager.glDeleteBuffers(sharedEBO);
}
@@ -3,6 +3,8 @@ package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.ffp.FfpFixture;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.glsm.testutil.TestThreads;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@@ -14,6 +16,7 @@ import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.SharedDrawable;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.concurrent.CountDownLatch;
@@ -29,14 +32,22 @@ import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@GLCoreTest
public class GLSM_SharedDrawableContext_GLTest {
private SharedDrawable sharedDrawable;
private int listVbo;
private int listEbo;
private int indexedList;
private int sharedContextVao;
@AfterEach
void cleanup() {
if (indexedList != 0) GLStateManager.glDeleteLists(indexedList, 1);
if (listVbo != 0) GLStateManager.glDeleteBuffers(listVbo);
if (listEbo != 0) GLStateManager.glDeleteBuffers(listEbo);
if (sharedDrawable != null) {
DrawableContexts.destroy(sharedDrawable);
sharedDrawable = null;
@@ -222,6 +233,113 @@ public class GLSM_SharedDrawableContext_GLTest {
});
}
private int compileIndexedListOnSharedDrawable() throws Exception {
sharedDrawable = new SharedDrawable(Display.getDrawable());
DrawableContexts.run(sharedDrawable, "SharedDrawableContext-IndexedList-Thread", () -> {
final FloatBuffer vertices = BufferUtils.createFloatBuffer(9);
vertices.put(new float[] { -0.5f, -0.5f, 0f, 0.5f, -0.5f, 0f, 0f, 0.5f, 0f });
vertices.flip();
final ByteBuffer indices = BufferUtils.createByteBuffer(6);
indices.putShort((short) 0).putShort((short) 1).putShort((short) 2);
indices.flip();
listVbo = GLStateManager.glGenBuffers();
listEbo = GLStateManager.glGenBuffers();
GLStateManager.glBindVertexArray(GLStateManager.glGenVertexArrays());
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, listVbo);
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, vertices, GL15.GL_STATIC_DRAW);
GLStateManager.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 12, 0L);
GLStateManager.glEnableVertexAttribArray(0);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, listEbo);
GLStateManager.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indices, GL15.GL_STATIC_DRAW);
indexedList = GLStateManager.glGenLists(1);
GLStateManager.glNewList(indexedList, GL11.GL_COMPILE);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glEndList();
final IndexedDrawBatch batch = indexedBatch();
batch.bind();
sharedContextVao = Reflect.<Integer>get(batch, "vao");
});
return indexedList;
}
private IndexedDrawBatch indexedBatch() {
return DisplayListManager.getDisplayList(indexedList).getIndexedBatches().get(0);
}
@Test
void indexedListCompiledOnSharedDrawableReplaysOnDisplay() throws Exception {
final int list = compileIndexedListOnSharedDrawable();
final ShaderManager sm = ShaderManager.getInstance();
final boolean wasEnabled = ShaderManager.isEnabled();
ShaderManager.enable();
sm.activate();
try {
GLStateManager.glCallList(list);
indexedBatch().bind();
assertEquals(indexedBatch().getSharedEBO(), GL11.glGetInteger(GL15.GL_ELEMENT_ARRAY_BUFFER_BINDING));
} finally {
sm.deactivate();
if (!wasEnabled) ShaderManager.disable();
GLStateManager.glBindVertexArray(0);
}
}
@Test
void deletingListLeavesDisplayVaoWithTheSharedContextName() throws Exception {
final int list = compileIndexedListOnSharedDrawable();
final IntBuffer made = BufferUtils.createIntBuffer(4096);
while (!GL30.glIsVertexArray(sharedContextVao) && made.hasRemaining()) {
final int v = GLStateManager.glGenVertexArrays();
made.put(v);
if (v == sharedContextVao) GLStateManager.glBindVertexArray(v);
}
GLStateManager.glBindVertexArray(0);
assumeTrue(GL30.glIsVertexArray(sharedContextVao));
GLStateManager.glDeleteLists(list, 1);
indexedList = 0;
assertTrue(GL30.glIsVertexArray(sharedContextVao));
made.flip();
while (made.hasRemaining()) GLStateManager.glDeleteVertexArrays(made.get());
}
@Test
void recordedVaoBindReplaysOnlyOnItsCompileContext() throws Exception {
final int bound = GLStateManager.glGenVertexArrays();
final int local = GLStateManager.glGenVertexArrays();
final int localList = GLStateManager.glGenLists(1);
final int foreignList = GLStateManager.glGenLists(1);
GLStateManager.glNewList(localList, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(local);
GLStateManager.glEndList();
sharedDrawable = new SharedDrawable(Display.getDrawable());
DrawableContexts.run(sharedDrawable, "SharedDrawableContext-VaoBind-Thread", () -> {
int foreign;
do foreign = GLStateManager.glGenVertexArrays(); while (foreign == bound);
GLStateManager.glNewList(foreignList, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(foreign);
GLStateManager.glEndList();
});
GLStateManager.glBindVertexArray(bound);
GLStateManager.glCallList(foreignList);
assertEquals(bound, GLStateManager.getBoundVAO());
GLStateManager.glCallList(localList);
assertEquals(local, GLStateManager.getBoundVAO());
GLStateManager.glBindVertexArray(0);
GLStateManager.glDeleteVertexArrays(bound);
GLStateManager.glDeleteVertexArrays(local);
GLStateManager.glDeleteLists(localList, 1);
GLStateManager.glDeleteLists(foreignList, 1);
}
@Test
void deferredTextureDeletesWaitForSplashAndScrubDrawableContexts() throws Exception {
sharedDrawable = new SharedDrawable(Display.getDrawable());
@@ -27,9 +27,10 @@ import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.params.provider.Arguments.arguments;
@@ -234,8 +235,9 @@ class IndexedDrawBatchBuilderTest extends DisplayListTestFixture {
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT);
replay(list);
final IndexedDrawBatch batch = firstBatch(list);
final int sharedVAO = batch.getSharedVAO();
final int sharedVAO = vaoOf(batch);
final int sharedVBO = batch.getSharedVBO();
final int sharedEBO = batch.getSharedEBO();
assertTrue(GL15.glIsBuffer(sharedVBO), "shared VBO live");
@@ -257,8 +259,9 @@ class IndexedDrawBatchBuilderTest extends DisplayListTestFixture {
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT);
replay(list);
final IndexedDrawBatch first = firstBatch(list);
final int oldVAO = first.getSharedVAO();
final int oldVAO = vaoOf(first);
final int oldVBO = first.getSharedVBO();
final int oldEBO = first.getSharedEBO();
@@ -341,8 +344,8 @@ class IndexedDrawBatchBuilderTest extends DisplayListTestFixture {
final List<IndexedDrawBatch> batches = batchesOf(list);
assertEquals(1, batches.size(), "same layout → one shared batch");
assertEquals(cmds.get(0).getSharedVAO(), cmds.get(1).getSharedVAO(), "shared VAO across draws in the same batch");
assertEquals(batches.getFirst().getSharedVAO(), cmds.get(0).getSharedVAO());
assertSame(batchOf(cmds.get(0)), batchOf(cmds.get(1)), "shared batch across draws in the same layout");
assertSame(batches.getFirst(), batchOf(cmds.get(0)));
assertEquals(0L, cmds.get(0).getIndexByteOffset());
assertEquals(3 * 4L, cmds.get(1).getIndexByteOffset(), "second draw offset = first.indexCount * sizeof(UINT)");
@@ -396,7 +399,7 @@ class IndexedDrawBatchBuilderTest extends DisplayListTestFixture {
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
assertEquals(2, cmds.size());
assertEquals(2, batchesOf(list).size(), "incompatible layouts → two batches");
assertNotEquals(cmds.get(0).getSharedVAO(), cmds.get(1).getSharedVAO(), "VAOs from separate batches must be distinct");
assertNotSame(batchOf(cmds.get(0)), batchOf(cmds.get(1)), "separate layouts must get distinct batches");
}
@Test
@@ -435,7 +438,7 @@ class IndexedDrawBatchBuilderTest extends DisplayListTestFixture {
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
assertEquals(2, cmds.size());
assertEquals(2, batchesOf(list).size(), "different enabled locations → two batches");
assertNotEquals(cmds.get(0).getSharedVAO(), cmds.get(1).getSharedVAO(), "VAOs from separate batches must be distinct");
assertNotSame(batchOf(cmds.get(0)), batchOf(cmds.get(1)), "separate layouts must get distinct batches");
}
@Test
@@ -495,13 +498,13 @@ class IndexedDrawBatchBuilderTest extends DisplayListTestFixture {
final List<BatchedIndexedDrawCmd> ba = allBatchedCmds(listBA);
// Placeholders sit in command-stream order (A-first vs B-first).
// Batches must be ordered so that placeholder[0] points at batch[0].
assertEquals(ab.get(0).getSharedVAO(), batchesOf(listAB).get(0).getSharedVAO(),
assertSame(batchesOf(listAB).get(0), batchOf(ab.get(0)),
"listAB: batch[0] must match the first-recorded layout");
assertEquals(ab.get(1).getSharedVAO(), batchesOf(listAB).get(1).getSharedVAO(),
assertSame(batchesOf(listAB).get(1), batchOf(ab.get(1)),
"listAB: batch[1] must match the second-recorded layout");
assertEquals(ba.get(0).getSharedVAO(), batchesOf(listBA).get(0).getSharedVAO(),
assertSame(batchesOf(listBA).get(0), batchOf(ba.get(0)),
"listBA: batch[0] must match the first-recorded layout");
assertEquals(ba.get(1).getSharedVAO(), batchesOf(listBA).get(1).getSharedVAO(),
assertSame(batchesOf(listBA).get(1), batchOf(ba.get(1)),
"listBA: batch[1] must match the second-recorded layout");
}
@@ -18,7 +18,10 @@ import java.util.List;
import static com.gtnewhorizons.angelica.glsm.GlTestSupport.drainErrors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -72,7 +75,8 @@ class IndexedDrawReplayTest extends DisplayListTestFixture {
assertNotNull(batched);
final IndexedDrawBatch batch = firstBatch(list);
GLStateManager.glBindVertexArray(batched.getSharedVAO());
replay(list);
GLStateManager.glBindVertexArray(vaoOf(batch));
assertEquals(batch.getSharedEBO(), GLStateManager.getBoundEBO(),
"batched VAO's EBO binding corrupted");
GLStateManager.glBindVertexArray(0);
@@ -120,8 +124,6 @@ class IndexedDrawReplayTest extends DisplayListTestFixture {
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
assertEquals(2, cmds.size());
final int sharedVAO = cmds.getFirst().getSharedVAO();
assertEquals(sharedVAO, cmds.get(1).getSharedVAO());
final int distractor = newVao();
GLStateManager.glBindVertexArray(distractor);
@@ -129,6 +131,9 @@ class IndexedDrawReplayTest extends DisplayListTestFixture {
compiledOf(list).render();
assertSame(batchOf(cmds.getFirst()), batchOf(cmds.get(1)));
final int sharedVAO = vaoOf(batchOf(cmds.getFirst()));
assertNotEquals(0, sharedVAO);
assertEquals(sharedVAO, GLStateManager.getBoundVAO());
GLStateManager.glBindVertexArray(0);
}
@@ -160,8 +165,8 @@ class IndexedDrawReplayTest extends DisplayListTestFixture {
try {
assertThrows(IllegalStateException.class, builder::build);
assertEquals(0, good.placeholder.getSharedVAO());
assertEquals(0, huge.placeholder.getSharedVAO());
assertNull(batchOf(good.placeholder));
assertNull(batchOf(huge.placeholder));
} finally {
MemoryUtilities.memFree(tinyVtx);
MemoryUtilities.memFree(tinyIdx);
@@ -8,6 +8,7 @@ import com.gtnewhorizons.angelica.glsm.GLTypes;
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
import com.gtnewhorizons.angelica.glsm.recording.commands.BatchedIndexedDrawCmd;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import org.junit.jupiter.api.AfterEach;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
@@ -176,6 +177,19 @@ public abstract class DisplayListTestFixture {
return batchesOf(listId).getFirst();
}
protected static void replay(int listId) {
compiledOf(listId).render();
GLStateManager.glBindVertexArray(0);
}
protected static IndexedDrawBatch batchOf(BatchedIndexedDrawCmd cmd) {
return Reflect.get(cmd, "batch");
}
protected static int vaoOf(IndexedDrawBatch batch) {
return Reflect.<Integer>get(batch, "vao");
}
protected static ByteBuffer readBufferBytes(int target, int bufferId, int size) {
final int prev;
if (target == GL15.GL_ARRAY_BUFFER) prev = GLStateManager.getBoundVBO();
+1 -1
View File
@@ -213,7 +213,7 @@ curseForgeRelations = requiredDependency\:gtnhlib
disableSpotless = true
# Uncomment this to disable Checkstyle checks (currently wildcard import check).
# disableCheckstyle = true
disableCheckstyle = true
# Override the IDEA build type. Valid values are: "" (leave blank, do not override), "idea" (force use native IDEA build), "gradle"
# (force use delegated build).
+4 -1
View File
@@ -6,7 +6,10 @@ import xyz.wagyourtail.jvmdg.gradle.task.files.DowngradeFiles
val libs = the<LibrariesForLibs>()
tasks.withType<DowngradeJar>().configureEach { logLevel.set("FATAL") }
tasks.withType<DowngradeFiles>().configureEach { logLevel.set("FATAL") }
tasks.withType<DowngradeFiles>().configureEach {
logLevel.set("FATAL")
outputs.cacheIf { true }
}
configurations.all {
resolutionStrategy.dependencySubstitution {
+3 -4
View File
@@ -7,7 +7,7 @@ asmCompile = "5.0.3"
launchwrapper = "1.12"
# End Frozen
lwjgl3 = "3.4.2-SNAPSHOT"
lwjgl3 = "3.4.2"
# Must equal the LWJGL release shipped by lwjgl3ify. Do not bump without bumping lwjgl3ify.
lwjgl3Release = "3.4.2"
lwjgl3ify = "3.0.31"
@@ -30,10 +30,9 @@ joml = "1.10.8"
commonsLang3 = "3.17.0"
jvmDowngrader = "1.3.5"
junit = "5.14.1"
asmTest = "9.7"
asmTest = "9.9.1"
asmBuild = "9.7"
mockito = "3.12.4"
archunit = "1.4.1"
ant = "1.8.2"
distantHorizonsApi = "5.1.0"
@@ -64,11 +63,11 @@ junit-platform-engine = { module = "org.junit.platform:junit-platform-engine" }
junit-platform-reporting = { module = "org.junit.platform:junit-platform-reporting" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
archunit-junit5 = { module = "com.tngtech.archunit:archunit-junit5", version.ref = "archunit" }
ant = { module = "org.apache.ant:ant", version.ref = "ant" }
distant-horizons-api = { module = "maven.modrinth:DistantHorizonsApi", version.ref = "distantHorizonsApi" }
asm-tree-compile = { module = "org.ow2.asm:asm-tree", version.ref = "asmCompile" }
asm-tree-test = { module = "org.ow2.asm:asm-tree", version.ref = "asmTest" }
asm-commons-test = { module = "org.ow2.asm:asm-commons", version.ref = "asmTest" }
asm-build = { module = "org.ow2.asm:asm", version.ref = "asmBuild" }
lwjgl2 = { module = "org.lwjgl.lwjgl:lwjgl", version.ref = "lwjgl2" }
lwjgl2-util = { module = "org.lwjgl.lwjgl:lwjgl_util", version.ref = "lwjgl2" }
+38 -8
View File
@@ -1,3 +1,5 @@
import java.time.Duration
plugins {
`java-library`
`maven-publish`
@@ -94,6 +96,7 @@ dependencies {
}
val glsmSdlTag = "glsm-sdl"
val sdlGpuDeviceTag = "sdl-gpu-device"
val glsmSdlStubs = sourceSets.create("glsmSdlStubs") {
java.srcDir("src/glsmSdlStubs/java")
@@ -109,15 +112,29 @@ dependencies {
testImplementation(glsmSdlAgent.output)
}
tasks.test {
useJUnitPlatform {
excludeTags(glsmSdlTag)
}
tasks.withType<Test>().configureEach {
if (System.getProperty("os.name").startsWith("Mac")) {
jvmArgs("-XstartOnFirstThread")
}
}
tasks.test {
useJUnitPlatform {
excludeTags(glsmSdlTag, sdlGpuDeviceTag)
}
}
val sdlGpuDeviceTest by tasks.registering(Test::class) {
description = "Runs the tests that need a real SDL GPU device."
group = "verification"
testClassesDirs = sourceSets["test"].output.classesDirs
classpath = sourceSets["test"].runtimeClasspath
useJUnitPlatform {
includeTags(sdlGpuDeviceTag)
}
mustRunAfter(tasks.test)
}
val glsmSdlRedirectAgentJar by tasks.registering(Jar::class) {
archiveClassifier = "glsm-sdl-redirect-agent"
from(glsmSdlAgent.output)
@@ -146,9 +163,6 @@ val glsmSdlTest by tasks.registering(Test::class) {
"-Dangelica.sdlgpu.disablePresenterThread=true",
"-Dceleritas.lwjglService=com.gtnewhorizons.angelica.sdlgpu.SDLGPULWJGLService",
)
if (System.getProperty("os.name").startsWith("Mac")) {
jvmArgs("-XstartOnFirstThread")
}
testLogging {
events("passed", "skipped", "failed")
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
@@ -156,7 +170,23 @@ val glsmSdlTest by tasks.registering(Test::class) {
mustRunAfter(tasks.test)
}
tasks.check { dependsOn(glsmSdlTest) }
val deviceTests = listOf(glsmSdlTest, sdlGpuDeviceTest)
deviceTests.forEach {
it.configure {
outputs.cacheIf { false }
timeout = Duration.ofMinutes(5)
systemProperty("junit.jupiter.execution.timeout.default", "60s")
systemProperty("junit.jupiter.execution.timeout.threaddump.enabled", "true")
}
}
val unpackLwjgl3Natives = registerLwjgl3NativesUnpack()
(deviceTests + tasks.test).forEach { it.configure { loadLwjgl3NativesFrom(unpackLwjgl3Natives) } }
if (!providers.environmentVariable("CI").isPresent) {
tasks.check { dependsOn(deviceTests) }
}
tasks.named<JavaCompile>("compileJava") {
val classesDir = destinationDirectory
@@ -3425,6 +3425,7 @@ public class SDLGPURenderBackend extends RenderBackend {
}
private void downloadBufferBlocking(long handle, int offset, int size, ByteBuffer out) {
if (size <= 0) return;
flushAndSubmitMidFrame();
final long cb = SDL_AcquireGPUCommandBuffer(device.getDevice());
if (cb == 0) return;
@@ -1585,6 +1585,7 @@ public final class ResourceManager {
public boolean arenaUpload(ByteBuffer data, long dstHandle, long dstOffset, boolean cycle) {
final FrameManager.FrameState f = frameManager.frame();
final int size = data.remaining();
if (size == 0) return true;
if (!f.arena.fits(size)) {
return false;
}
@@ -1682,6 +1683,7 @@ public final class ResourceManager {
if (srcOff < 0 || size < 0 || srcOff + size > src.capacity()) {
throw new IllegalStateException("uploadRangeToBuffer: bad source range srcOff=" + srcOff + " size=" + size + " src.capacity=" + src.capacity());
}
if (size == 0) return;
final long xfer = acquireTransferBuffer(size);
if (xfer == 0) return;
@@ -10,7 +10,7 @@ import org.lwjgl.sdl.SDLError;
import org.lwjgl.sdl.SDLHints;
import org.lwjgl.sdl.SDLInit;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.sdl.SDLGPU.SDL_CreateGPUDevice;
import static org.lwjgl.sdl.SDLGPU.SDL_DestroyGPUDevice;
import static org.lwjgl.sdl.SDLGPU.SDL_GetGPUDeviceDriver;
@@ -90,13 +90,13 @@ public final class SdlTestRig {
public static void initSdlVideo() {
SDLHints.SDL_SetHint(SDLHints.SDL_HINT_MAC_BACKGROUND_APP, "1");
assumeTrue(SDLInit.SDL_Init(SDL_INIT_VIDEO), () -> "SDL_Init failed: " + SDLError.SDL_GetError());
assertTrue(SDLInit.SDL_Init(SDL_INIT_VIDEO), () -> "SDL_Init failed: " + SDLError.SDL_GetError());
}
public static SdlTestRig acquireRealDevice() {
initSdlVideo();
realSdlDevice = SDL_CreateGPUDevice(SHADER_FORMATS, true, (CharSequence) null);
assumeTrue(realSdlDevice != 0, "No SDL GPU device: " + SDLError.SDL_GetError());
assertTrue(realSdlDevice != 0, "No SDL GPU device: " + SDLError.SDL_GetError());
final SdlTestRig rig = new SdlTestRig(realSdlDevice);
rig.frameManager.setResourceManager(rig.resourceManager);
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import it.unimi.dsi.fastutil.longs.Long2BooleanOpenHashMap;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -13,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@Tag("sdl-gpu-device")
class DeviceImageAtomicsGpuTest {
private Device device;
@@ -34,7 +36,7 @@ class DeviceImageAtomicsGpuTest {
@Test
void verdictsAreCachedPerSizeAndDroppedWithTheDevice() {
device = new Device();
assumeTrue(device.createDevice(), "no SDL GPU device on this machine");
assertTrue(device.createDevice(), "no SDL GPU device on this machine");
assumeTrue(device.metalTextureAtomics(), "needs native Metal compiling at MSL 3.1 or newer");
assertFalse(device.supportsImageAtomics(40, 10, 2));
@@ -3,13 +3,14 @@ package com.gtnewhorizons.angelica.sdlgpu.device;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@Tag("sdl-gpu-device")
class DeviceLifecycleGpuTest {
private Device device;
@@ -27,7 +28,7 @@ class DeviceLifecycleGpuTest {
@Test
void createsWithoutAWindowAndIsMemoized() {
device = new Device();
assumeTrue(device.createDevice(), "no SDL GPU device on this machine");
assertTrue(device.createDevice(), "no SDL GPU device on this machine");
final long handle = device.getDevice();
assertNotEquals(0L, handle);
@@ -38,7 +39,7 @@ class DeviceLifecycleGpuTest {
@Test
void destroyIsIdempotentOnAnUnclaimedDevice() {
device = new Device();
assumeTrue(device.createDevice(), "no SDL GPU device on this machine");
assertTrue(device.createDevice(), "no SDL GPU device on this machine");
device.destroyDevice();
assertEquals(0L, device.getDevice());
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.sdlgpu.device;
import com.gtnewhorizons.angelica.sdlgpu.RealDeviceExtension;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.system.MemoryStack;
@@ -18,6 +19,7 @@ import static org.lwjgl.sdl.SDLVersion.SDL_GetVersion;
import static org.lwjgl.sdl.SDLVersion.SDL_VERSIONNUM;
@ExtendWith(RealDeviceExtension.class)
@Tag("sdl-gpu-device")
class FenceProbeRealDeviceTest {
@Test
@@ -0,0 +1,66 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL40;
import org.lwjgl.opengl.GL43;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Tag("glsm-sdl")
@ExtendWith(GlsmSdlFrameExtension.class)
class GlsmSdlEmptyBufferTransferTest {
private int id;
private ByteBuffer empty;
@BeforeEach
void setUp() {
id = GLStateManager.glGenBuffers();
empty = MemoryUtil.memAlloc(16);
empty.limit(0);
}
@AfterEach
void tearDown() {
MemoryUtil.memFree(empty);
GLStateManager.glDeleteBuffers(id);
}
@Test
void emptyBufferDataReplacesTheStoreWithoutACopy() {
final int target = GL40.GL_DRAW_INDIRECT_BUFFER;
GLStateManager.glBindBuffer(target, id);
GLStateManager.glBufferData(target, 256L, GL15.GL_STATIC_DRAW);
GlsmSdlHeadlessRig.resourceManager().flushUploadArena();
GLStateManager.glBufferData(target, empty, GL15.GL_STATIC_DRAW);
assertEquals(0L, GlsmSdlHeadlessRig.frameManager().frame().arenaCopyPass);
assertEquals(0, GLStateManager.glGetBufferParameteri(target, GL15.GL_BUFFER_SIZE));
}
@Test
void emptyStorageBufferSubDataIssuesNoCopy() {
final int target = GL43.GL_SHADER_STORAGE_BUFFER;
GLStateManager.glBindBuffer(target, id);
GLStateManager.glBufferData(target, 256L, GL15.GL_DYNAMIC_DRAW);
final int before = GlsmSdlHeadlessRig.frameManager().frame().pendingUploadCommands;
GLStateManager.glBufferSubData(target, 0, empty);
assertEquals(before, GlsmSdlHeadlessRig.frameManager().frame().pendingUploadCommands);
}
@Test
void emptyGetBufferSubDataDoesNotReachTheDevice() {
final int target = GL43.GL_SHADER_STORAGE_BUFFER;
GLStateManager.glBindBuffer(target, id);
GLStateManager.glBufferData(target, 256L, GL15.GL_DYNAMIC_DRAW);
GLStateManager.glGetBufferSubData(target, 0, empty);
}
}
@@ -24,7 +24,6 @@ import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
public final class GlsmSdlHeadlessRig {
@@ -48,7 +47,7 @@ public final class GlsmSdlHeadlessRig {
public static void boot() {
if (booted) return;
SdlTestRig.initSdlVideo();
assumeTrue(SDLGPUGate.device().createDevice(), () -> "no SDL GPU device: " + SDLError.SDL_GetError());
assertTrue(SDLGPUGate.device().createDevice(), () -> "no SDL GPU device: " + SDLError.SDL_GetError());
Reflect.setStatic(SDLGPUGate.class, "engaged", true);
final RenderBackend backend = BackendManager.RENDER_BACKEND;
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -13,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(RealDeviceExtension.class)
@Tag("sdl-gpu-device")
class AttachmentClearTest {
private static SdlTestRig rig;
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager.ProgramObject;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager.UniformMemberInfo;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.lwjgl.util.spvc.Spvc;
@@ -97,6 +98,7 @@ class PerFrameBlockDedupTest {
}
@Test
@Tag("sdl-gpu-device")
void flushUniformBlocksEndsTheCopyPassOnceForTwoDirtyBlocks() throws Exception {
final SdlTestRig rig = SdlTestRig.acquireRealDevice();
try {
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL20;
@@ -21,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.sdl.SDLGPU.*;
@ExtendWith(RealDeviceExtension.class)
@Tag("sdl-gpu-device")
class StencilMaskGpuTest {
private static SdlTestRig rig;
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL15;
@@ -14,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(RealDeviceExtension.class)
@Tag("sdl-gpu-device")
class BufferSizeGpuExactnessTest {
private static Device device;
@@ -7,6 +7,7 @@ import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -21,6 +22,7 @@ import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
@ExtendWith(RealDeviceExtension.class)
@Timeout(120)
@Tag("sdl-gpu-device")
class CopyTexSubImageFromFbo0GpuTest {
private static final int SIZE = 64;
@@ -8,6 +8,7 @@ import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -28,6 +29,7 @@ import static org.lwjgl.system.MemoryStack.stackPush;
@ExtendWith(RealDeviceExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Tag("sdl-gpu-device")
class UploadArenaGpuRoundTripTest {
private static Device device;
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.sdlgpu.shader;
import com.gtnewhorizons.angelica.sdlgpu.RealDeviceExtension;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -31,6 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.sdl.SDLGPU.*;
@ExtendWith(RealDeviceExtension.class)
@Tag("sdl-gpu-device")
class FragmentStorageTextureReadGpuTest {
private static SdlTestRig rig;
@@ -85,6 +85,12 @@ public enum Mixins implements IMixins {
.addClientMixins("client.starminer.MixinTransformClientHelper")
),
RAILCRAFT_LANTERN_CONNECTIONS(new MixinBuilder("Fix Railcraft lanterns rendering with Angelica")
.setPhase(Phase.LATE)
.addRequiredMod(TargetedMod.RAILCRAFT)
.addClientMixins("client.railcraft.MixinRenderBlockLamp")
),
ANGELICA_ENTITY_OVERLAYS(new MixinBuilder()
.setPhase(Phase.EARLY)
.setApplyIf(() -> AngelicaConfig.entityOverlayFixes)
@@ -27,6 +27,7 @@ public enum TargetedMod implements ITargetMod {
IC2("ic2.core.coremod.IC2core", "IC2"),
NATURA(null, "Natura"),
OPENBLOCKS(null, "OpenBlocks"),
RAILCRAFT(null, "Railcraft"),
SECURITYCRAFT(null, "securitycraft"),
STARMINER("jp.mc.ancientred.starminer.core.SMPlugin", "modJ_StarMiner"),
SUPERNOVA("com.mitchej123.supernova.core.SupernovaCore", "supernova"),
@@ -36,7 +36,6 @@ import org.embeddedt.embeddium.impl.render.chunk.RenderPassConfiguration;
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildBuffers;
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildContext;
import org.embeddedt.embeddium.impl.render.chunk.data.MinecraftBuiltRenderSectionData;
import org.embeddedt.embeddium.impl.render.chunk.sprite.SpriteTransparencyLevel;
import org.embeddedt.embeddium.impl.render.chunk.terrain.material.Material;
import org.embeddedt.embeddium.impl.render.chunk.vertex.format.ChunkVertexEncoder;
import org.embeddedt.embeddium.impl.util.QuadUtil;
@@ -62,6 +61,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
@Getter private final FloatArrayList teBoundsScratch = new FloatArrayList();
@Getter private final long[] deferredMask = new long[64];
private final VertexArrayQuadView quadView;
private final QuadMaterialSelector materialSelector = new QuadMaterialSelector();
private final boolean hasColoredLight;
private final float[] tintResult = new float[3];
private List<IDynamicLightSource> chunkLightSources;
@@ -116,27 +116,6 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
}
}
private Material selectMaterial(Material material, TextureAtlasSprite sprite, boolean isShaderPackOverride,
boolean useRenderPassOptimization) {
// Don't apply transparency-based optimization when shader pack explicitly overrides the material
if (isShaderPackOverride) {
return material;
}
if (useRenderPassOptimization
&& sprite != null && sprite.getClass() == TextureAtlasSprite.class && !sprite.hasAnimationMetadata()) {
final var transparencyLevel = ((SpriteExtension) sprite).celeritas$getTransparencyLevel();
if (transparencyLevel == SpriteTransparencyLevel.OPAQUE
&& material == AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL) {
return AngelicaRenderPassConfiguration.SOLID_MATERIAL;
} else if (material == AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL && transparencyLevel != SpriteTransparencyLevel.TRANSLUCENT) {
return AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL;
}
}
return material;
}
@SuppressWarnings("unchecked")
public void copyRawBuffer(int[] rawBuffer, int vertexCount, int[] vertexStates,
int[] shaderOverrideBlockIds,
@@ -174,9 +153,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
final short originalBlockId = blockRenderContext.blockId;
int stateIdx = 0;
boolean hasCachedMaterial = false;
TextureAtlasSprite lastSprite = null;
Material lastMaterial = null;
materialSelector.reset(material, isShaderPackOverride, useRenderPassOptimization);
this.fluidQuadLightFace = null;
for (int quadIdx = 0; quadIdx < numQuads; quadIdx++) {
@@ -266,17 +243,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
// Apply RGB block light tint from provider
applyBlockLightTint(worldX, worldY, worldZ, vertices);
final Material correctMaterial;
if (material == AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL && hasVertexTransparency) {
correctMaterial = material;
} else if (hasCachedMaterial && sprite == lastSprite) {
correctMaterial = lastMaterial;
} else {
correctMaterial = selectMaterial(material, sprite, isShaderPackOverride, useRenderPassOptimization);
lastSprite = sprite;
lastMaterial = correctMaterial;
hasCachedMaterial = true;
}
final Material correctMaterial = materialSelector.select(facing, sprite, hasVertexTransparency);
final var builder = buffers.get(correctMaterial);
final int shaderOverrideBlockId = shaderOverrideBlockIds[quadIdx * 4];
@@ -0,0 +1,57 @@
package com.gtnewhorizons.angelica.rendering.celeritas;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import org.embeddedt.embeddium.impl.model.quad.properties.ModelQuadFacing;
import org.embeddedt.embeddium.impl.render.chunk.sprite.SpriteTransparencyLevel;
import org.embeddedt.embeddium.impl.render.chunk.terrain.material.Material;
class QuadMaterialSelector {
private Material material;
private boolean canDemote;
private int facesAtBaseMaterial;
private TextureAtlasSprite lastSprite;
private Material lastMaterial;
void reset(Material material, boolean isShaderPackOverride, boolean useRenderPassOptimization) {
this.material = material;
// Don't apply transparency-based optimization when shader pack explicitly overrides the material
this.canDemote = useRenderPassOptimization && !isShaderPackOverride;
facesAtBaseMaterial = 0;
lastMaterial = null;
}
Material select(ModelQuadFacing facing, TextureAtlasSprite sprite, boolean hasVertexTransparency) {
final int faceBit = 1 << facing.ordinal();
final Material correctMaterial;
if ((facesAtBaseMaterial & faceBit) != 0 || (material == AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL && hasVertexTransparency)) {
correctMaterial = material;
} else if (lastMaterial != null && sprite == lastSprite) {
correctMaterial = lastMaterial;
} else {
correctMaterial = demote(sprite);
lastSprite = sprite;
lastMaterial = correctMaterial;
}
if (correctMaterial == material) {
facesAtBaseMaterial |= faceBit;
}
return correctMaterial;
}
private Material demote(TextureAtlasSprite sprite) {
if (canDemote && sprite != null && sprite.getClass() == TextureAtlasSprite.class && !sprite.hasAnimationMetadata()) {
final var transparencyLevel = transparencyLevel(sprite);
if (transparencyLevel == SpriteTransparencyLevel.OPAQUE && material == AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL) {
return AngelicaRenderPassConfiguration.SOLID_MATERIAL;
} else if (material == AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL && transparencyLevel != SpriteTransparencyLevel.TRANSLUCENT) {
return AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL;
}
}
return material;
}
SpriteTransparencyLevel transparencyLevel(TextureAtlasSprite sprite) {
return ((SpriteExtension) sprite).celeritas$getTransparencyLevel();
}
}
@@ -0,0 +1,22 @@
package com.gtnewhorizons.angelica.mixins.late.client.railcraft;
import net.minecraft.world.ChunkCache;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
@Pseudo
@Mixin(targets = "mods.railcraft.client.render.RenderBlockLamp", remap = false)
public class MixinRenderBlockLamp {
@ModifyConstant(
method = "canConnect(Lnet/minecraft/world/IBlockAccess;IIILnet/minecraftforge/common/util/ForgeDirection;)Z",
constant = @Constant(classValue = ChunkCache.class),
require = 0,
expect = 0)
private boolean angelica$acceptAnyBlockAccess(Object world, Class<?> type) {
return true;
}
}
@@ -0,0 +1,181 @@
package com.gtnewhorizons.angelica.arch;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.ClassRemapper;
import org.objectweb.asm.commons.Remapper;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
final class ArchScan {
enum Dir {
MAIN("build/classes/java/main"),
MIXIN("build/classes/java/mixin"),
GLSM("glsm/build/classes/java/main"),
LWJGL3_BACKEND("lwjgl3-backend/build/classes/java/main"),
SDL_GPU("sdl-gpu/build/classes/java/main");
final String path;
Dir(String path) {
this.path = path;
}
}
enum Kind { FIELD, CALL }
record Access(String method, Kind kind, String owner, String name, String desc) {
String target() {
return owner + "." + name + (kind == Kind.FIELD ? " " : "") + desc;
}
List<String> descriptorTypes() {
return kind == Kind.FIELD ? Collections.singletonList(Type.getType(desc).getClassName()) : signatureTypes(desc);
}
}
record ClassConstant(String method, String type) {
}
record Member(String name, String desc) {
}
record ScannedClass(Dir dir, String name, Set<String> annotations, Set<String> references, List<Member> methods, List<Access> accesses, List<ClassConstant> classConstants) {
}
private static final List<ScannedClass> CLASSES = scan();
private static final Map<String, ScannedClass> BY_NAME = index(CLASSES);
private ArchScan() {
}
static List<ScannedClass> in(Dir... dirs) {
final List<Dir> wanted = Arrays.asList(dirs);
final List<ScannedClass> selected = new ArrayList<>();
for (ScannedClass c : CLASSES) {
if (wanted.contains(c.dir())) selected.add(c);
}
return selected;
}
static List<String> signatureTypes(String desc) {
final List<String> types = new ArrayList<>();
for (Type argument : Type.getArgumentTypes(desc)) {
types.add(argument.getClassName());
}
types.add(Type.getReturnType(desc).getClassName());
return types;
}
static ScannedClass find(String name) {
return BY_NAME.get(name);
}
static ScannedClass get(String name) {
final ScannedClass c = BY_NAME.get(name);
if (c == null) throw new IllegalStateException(name + " was not scanned; the scan paths are likely wrong");
return c;
}
private static Map<String, ScannedClass> index(List<ScannedClass> classes) {
final Map<String, ScannedClass> byName = new HashMap<>();
for (ScannedClass c : classes) {
byName.put(c.name(), c);
}
return byName;
}
private static List<ScannedClass> scan() {
final List<ScannedClass> classes = new ArrayList<>();
for (Dir dir : Dir.values()) {
final Path root = Paths.get(dir.path);
if (!Files.isDirectory(root)) throw new IllegalStateException(dir.path + " is not a directory");
final int before = classes.size();
try (Stream<Path> files = Files.walk(root)) {
for (final Iterator<Path> it = files.iterator(); it.hasNext(); ) {
final Path file = it.next();
if (file.toString().endsWith(".class")) classes.add(read(dir, Files.readAllBytes(file)));
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
if (classes.size() == before) throw new IllegalStateException(dir.path + " yielded no classes");
}
return classes;
}
private static ScannedClass read(Dir dir, byte[] bytes) {
final Set<String> references = new HashSet<>();
final ClassNode node = new ClassNode();
final Remapper recorder = new Remapper() {
@Override
public String map(String internalName) {
references.add(className(internalName));
return internalName;
}
};
new ClassReader(bytes).accept(new ClassRemapper(node, recorder), ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
final String name = className(node.name);
references.remove(name);
final Set<String> annotations = new HashSet<>();
if (node.visibleAnnotations != null) {
for (AnnotationNode annotation : node.visibleAnnotations) {
annotations.add(Type.getType(annotation.desc).getClassName());
}
}
final List<Member> methods = new ArrayList<>();
final List<Access> accesses = new ArrayList<>();
final List<ClassConstant> classConstants = new ArrayList<>();
for (MethodNode method : node.methods) {
methods.add(new Member(method.name, method.desc));
final String origin = method.name + method.desc;
for (AbstractInsnNode insn : method.instructions) {
if (insn instanceof FieldInsnNode field) {
accesses.add(new Access(origin, Kind.FIELD, className(field.owner), field.name, field.desc));
} else if (insn instanceof MethodInsnNode call) {
accesses.add(new Access(origin, Kind.CALL, className(call.owner), call.name, call.desc));
} else if (insn instanceof InvokeDynamicInsnNode dynamic) {
for (Object argument : dynamic.bsmArgs) {
if (argument instanceof Handle handle) accesses.add(new Access(origin, handle.getTag() <= Opcodes.H_PUTSTATIC ? Kind.FIELD : Kind.CALL, className(handle.getOwner()), handle.getName(), handle.getDesc()));
}
} else if (insn instanceof LdcInsnNode ldc && ldc.cst instanceof Type type && (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY)) {
classConstants.add(new ClassConstant(origin, type.getClassName()));
}
}
}
return new ScannedClass(dir, name, annotations, references, methods, accesses, classConstants);
}
private static String className(String internalName) {
return Type.getObjectType(internalName).getClassName();
}
}
@@ -1,9 +1,8 @@
package com.gtnewhorizons.angelica.arch;
import com.tngtech.archunit.core.domain.JavaAccess;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.gtnewhorizons.angelica.arch.ArchScan.Access;
import com.gtnewhorizons.angelica.arch.ArchScan.Dir;
import com.gtnewhorizons.angelica.arch.ArchScan.ScannedClass;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
@@ -18,23 +17,20 @@ class EarlyLoadingBoundaryTest {
private static final String TRACY = "com.gtnewhorizons.angelica.glsm.profiling.Tracy";
private static final Pattern GUARDED = Pattern.compile(
".*/com/gtnewhorizons/angelica/(loading/[^/]+|mixins/Mixins(\\$[^/]*)?|AngelicaMod(\\$[^/]*)?|config/AngelicaConfig(\\$[^/]*)?)\\.class$");
private static final JavaClasses classes = new ClassFileImporter()
.withImportOption(location -> location.matches(GUARDED))
.importPaths("build/classes/java/main");
"com\\.gtnewhorizons\\.angelica\\.(loading\\.[^.]+|mixins\\.Mixins(\\$[^.]*)?|AngelicaMod(\\$[^.]*)?|config\\.AngelicaConfig(\\$[^.]*)?)");
@Test
void earlyLoadingClassesDoNotAccessTracy() {
assertFalse(classes.isEmpty(), "no guarded classes imported");
int guarded = 0;
final List<String> violations = new ArrayList<>();
for (JavaClass origin : classes) {
for (JavaAccess<?> access : origin.getAccessesFromSelf()) {
if (access.getTargetOwner().getName().equals(TRACY)) {
violations.add(origin.getFullName() + " -> " + access.getTarget().getFullName());
}
for (ScannedClass origin : ArchScan.in(Dir.MAIN)) {
if (!GUARDED.matcher(origin.name()).matches()) continue;
guarded++;
for (Access access : origin.accesses()) {
if (access.owner().equals(TRACY)) violations.add(origin.name() + "." + access.method() + " -> " + access.target());
}
}
assertFalse(guarded == 0, "no guarded classes scanned");
assertTrue(violations.isEmpty(), "Coremod-phase code must not touch glsm.profiling.Tracy:\n" + String.join("\n", violations));
}
}
@@ -1,17 +1,15 @@
package com.gtnewhorizons.angelica.arch;
import com.tngtech.archunit.core.domain.AccessTarget;
import com.tngtech.archunit.core.domain.JavaAccess;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.gtnewhorizons.angelica.arch.ArchScan.Access;
import com.gtnewhorizons.angelica.arch.ArchScan.Dir;
import com.gtnewhorizons.angelica.arch.ArchScan.Member;
import com.gtnewhorizons.angelica.arch.ArchScan.ScannedClass;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -29,24 +27,16 @@ class Lwjgl3AwareBoundaryTest {
"com.gtnewhorizons.angelica.sdlgpu.",
"com.gtnewhorizons.angelica.lwjgl3.");
private static final JavaClasses classes = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPaths(
"build/classes/java/main",
"build/classes/java/mixin",
"glsm/build/classes/java/main",
"lwjgl3-backend/build/classes/java/main",
"sdl-gpu/build/classes/java/main");
private static final List<ScannedClass> classes = ArchScan.in(Dir.values());
@Test
void awareClassesDoNotReadRemappedMembers() {
final List<String> violations = new ArrayList<>();
for (JavaClass origin : classes) {
if (!isAware(origin)) continue;
for (JavaAccess<?> access : origin.getAccessesFromSelf()) {
final JavaClass owner = access.getTargetOwner();
if (isUntransformed(owner) || isAware(owner)) continue;
if (mentionsLwjgl3Type(access.getTarget())) violations.add(describe(access));
for (ScannedClass origin : classes) {
if (!isAware(origin.name())) continue;
for (Access access : origin.accesses()) {
if (isUntransformed(access.owner()) || isAware(access.owner())) continue;
if (mentionsLwjgl3Type(access)) violations.add(describe(origin, access));
}
}
assertTrue(violations.isEmpty(), "@Lwjgl3Aware code referencing a remapped member with an org.lwjgl type in its descriptor:\n" + String.join("\n", violations));
@@ -55,11 +45,11 @@ class Lwjgl3AwareBoundaryTest {
@Test
void remappedClassesDoNotReadAwareMembers() {
final List<String> violations = new ArrayList<>();
for (JavaClass origin : classes) {
if (isAware(origin) || isUntransformed(origin)) continue;
for (JavaAccess<?> access : origin.getAccessesFromSelf()) {
if (!isAware(access.getTargetOwner())) continue;
if (mentionsLwjgl3Type(access.getTarget())) violations.add(describe(access));
for (ScannedClass origin : classes) {
if (isAware(origin.name()) || isUntransformed(origin.name())) continue;
for (Access access : origin.accesses()) {
if (!isAware(access.owner())) continue;
if (mentionsLwjgl3Type(access)) violations.add(describe(origin, access));
}
}
assertTrue(violations.isEmpty(), "Remapped code referencing an @Lwjgl3Aware member with an org.lwjgl type in its descriptor:\n" + String.join("\n", violations));
@@ -69,64 +59,51 @@ class Lwjgl3AwareBoundaryTest {
@Test
void nativeLwjgl3ModulesAreFullyAware() {
final List<String> missing = new ArrayList<>();
for (JavaClass c : classes) {
if (AWARE_MODULES.stream().anyMatch(p -> c.getName().startsWith(p)) && !isAware(c)) {
missing.add(c.getName());
}
int inAwareModules = 0;
for (ScannedClass c : classes) {
if (AWARE_MODULES.stream().noneMatch(p -> c.name().startsWith(p))) continue;
inAwareModules++;
if (!isAware(c.name())) missing.add(c.name());
}
assertFalse(inAwareModules == 0, "no classes scanned under " + AWARE_MODULES);
assertTrue(missing.isEmpty(), "Classes missing @Lwjgl3Aware:\n" + String.join("\n", missing));
}
@Test
void renderBackendSignaturesAreRemapSafe() {
final JavaClass renderBackend = classes.get("com.gtnewhorizons.angelica.glsm.backend.RenderBackend");
final ScannedClass renderBackend = ArchScan.get("com.gtnewhorizons.angelica.glsm.backend.RenderBackend");
final List<String> violations = new ArrayList<>();
for (JavaMethod method : renderBackend.getMethods()) {
final List<JavaClass> types = new ArrayList<>(method.getRawParameterTypes());
types.add(method.getRawReturnType());
if (types.stream().anyMatch(Lwjgl3AwareBoundaryTest::isRemapUnsafeType)) {
violations.add(method.getFullName());
for (Member method : renderBackend.methods()) {
if (method.name().startsWith("<")) continue;
if (ArchScan.signatureTypes(method.desc()).stream().anyMatch(Lwjgl3AwareBoundaryTest::isRemapUnsafeType)) {
violations.add(renderBackend.name() + "." + method.name() + method.desc());
}
}
assertTrue(violations.isEmpty(), "RenderBackend methods with an org.lwjgl/org.lwjglx/lwjgl3ify type in their signature:\n" + String.join("\n", violations));
}
private static boolean isAware(JavaClass c) {
return c.isAnnotatedWith(AWARE);
private static boolean isAware(String name) {
final ScannedClass c = ArchScan.find(name);
return c != null && c.annotations().contains(AWARE);
}
private static boolean isUntransformed(JavaClass c) {
return UNTRANSFORMED_PACKAGES.stream().anyMatch(p -> c.getName().startsWith(p));
private static boolean isUntransformed(String name) {
return UNTRANSFORMED_PACKAGES.stream().anyMatch(name::startsWith);
}
private static boolean isLwjgl3Type(JavaClass type) {
return type.getName().startsWith("org.lwjgl.");
private static boolean isLwjgl3Type(String name) {
return name.startsWith("org.lwjgl.");
}
private static boolean isRemapUnsafeType(JavaClass type) {
final String name = type.getName();
return isLwjgl3Type(type) || name.startsWith("org.lwjglx.") || name.startsWith("me.eigenraven.lwjgl3ify.");
private static boolean isRemapUnsafeType(String name) {
return isLwjgl3Type(name) || name.startsWith("org.lwjglx.") || name.startsWith("me.eigenraven.lwjgl3ify.");
}
private static boolean mentionsLwjgl3Type(AccessTarget target) {
return descriptorTypes(target).stream().anyMatch(Lwjgl3AwareBoundaryTest::isLwjgl3Type);
private static boolean mentionsLwjgl3Type(Access access) {
return access.descriptorTypes().stream().anyMatch(Lwjgl3AwareBoundaryTest::isLwjgl3Type);
}
private static List<JavaClass> descriptorTypes(AccessTarget target) {
if (target instanceof AccessTarget.FieldAccessTarget field) {
return List.of(field.getRawType());
}
if (target instanceof AccessTarget.CodeUnitCallTarget call) {
final List<JavaClass> types = new ArrayList<>(call.getRawParameterTypes());
if (target instanceof AccessTarget.MethodCallTarget method) {
types.add(method.getRawReturnType());
}
return types;
}
return List.of();
}
private static String describe(JavaAccess<?> access) {
return " " + access.getOwner().getFullName() + " -> " + access.getTarget().getFullName();
private static String describe(ScannedClass origin, Access access) {
return " " + origin.name() + "." + access.method() + " -> " + access.target();
}
}
@@ -1,15 +1,16 @@
package com.gtnewhorizons.angelica.arch;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchRule;
import com.gtnewhorizons.angelica.arch.ArchScan.Access;
import com.gtnewhorizons.angelica.arch.ArchScan.Dir;
import com.gtnewhorizons.angelica.arch.ArchScan.Kind;
import com.gtnewhorizons.angelica.arch.ArchScan.ScannedClass;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.core.domain.JavaCall.Predicates.target;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.nameMatching;
import static com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With.owner;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Enforces that all GL calls go through GLStateManager / RENDER_BACKEND. Direct method calls on org.lwjgl.opengl GL command classes are banned outside
@@ -23,27 +24,22 @@ class NoDirectGLCallsTest {
// Matches GL command classes but not LWJGL infrastructure (Display, etc.)
// Covers: GL11-GL44, ARB*, EXT*, NV*, AMD*, ATI*, KHR*, INTEL*, GLContext, plus OpenGlHelper
private static final String GL_COMMAND_CLASSES = "org\\.lwjgl\\.opengl\\.(?!Display|ContextAttribs|PixelFormat|Drawable|SharedDrawable).*";
private static final Pattern GL_COMMAND_CLASSES = Pattern.compile("org\\.lwjgl\\.opengl\\.(?!Display|ContextAttribs|PixelFormat|Drawable|SharedDrawable).*");
private static final JavaClasses classes = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPaths(
"build/classes/java/main",
"build/classes/java/mixin"
);
private static final ArchRule NO_DIRECT_GL_CALLS = noClasses()
.that().resideOutsideOfPackages(
"com.gtnewhorizons.angelica.glsm.backend..",
"com.gtnewhorizons.angelica.loading.."
)
.should().callMethodWhere(
target(owner(nameMatching(GL_COMMAND_CLASSES)))
)
.because("GL calls must go through GLStateManager, not directly to LWJGL or the RENDER_BACKEND");
private static final List<String> EXEMPT_PACKAGES = List.of(
"com.gtnewhorizons.angelica.glsm.backend.",
"com.gtnewhorizons.angelica.loading.");
@Test
void noDirectGLCalls() {
NO_DIRECT_GL_CALLS.check(classes);
final List<String> violations = new ArrayList<>();
for (ScannedClass origin : ArchScan.in(Dir.MAIN, Dir.MIXIN)) {
if (EXEMPT_PACKAGES.stream().anyMatch(origin.name()::startsWith)) continue;
for (Access access : origin.accesses()) {
if (access.kind() != Kind.CALL || access.name().equals("<init>")) continue;
if (GL_COMMAND_CLASSES.matcher(access.owner()).matches()) violations.add(" " + origin.name() + "." + access.method() + " -> " + access.target());
}
}
assertTrue(violations.isEmpty(), "GL calls must go through GLStateManager, not directly to LWJGL or the RENDER_BACKEND:\n" + String.join("\n", violations));
}
}
@@ -0,0 +1,43 @@
package com.gtnewhorizons.angelica.arch;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class NoWildcardImportsTest {
private static final List<String> SOURCE_DIRS = List.of("src/main/java", "src/mixin/java", "src/test/java");
private static final Pattern WILDCARD_IMPORT = Pattern.compile("\\s*import\\s+(?!static\\s)[\\w.]+\\.\\*\\s*;.*");
@Test
void noWildcardImports() throws IOException {
final List<String> violations = new ArrayList<>();
for (String dir : SOURCE_DIRS) {
final List<Path> sources;
try (Stream<Path> files = Files.walk(Paths.get(dir))) {
sources = files.filter(file -> file.toString().endsWith(".java")).collect(Collectors.toList());
}
assertFalse(sources.isEmpty(), dir + " has no Java sources");
for (Path source : sources) {
final String[] lines = new String(Files.readAllBytes(source), StandardCharsets.ISO_8859_1).split("\r?\n");
for (int i = 0; i < lines.length; i++) {
if (WILDCARD_IMPORT.matcher(lines[i]).matches()) violations.add(" " + source + ":" + (i + 1));
}
}
}
assertTrue(violations.isEmpty(), "Wildcard imports (static ones are allowed):\n" + String.join("\n", violations));
}
}
@@ -1,21 +1,16 @@
package com.gtnewhorizons.angelica.arch;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaAccess;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.ReferencedClassObject;
import com.tngtech.archunit.core.domain.JavaStaticInitializer;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchRule;
import com.gtnewhorizons.angelica.arch.ArchScan.Access;
import com.gtnewhorizons.angelica.arch.ArchScan.ClassConstant;
import com.gtnewhorizons.angelica.arch.ArchScan.Dir;
import com.gtnewhorizons.angelica.arch.ArchScan.ScannedClass;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -29,69 +24,59 @@ class SDLGPUEncapsulationTest {
private static final String LWJGL3_AWARE = "me.eigenraven.lwjgl3ify.api.Lwjgl3Aware";
private static final DescribedPredicate<JavaClass> SDLGPU_BEHIND_THE_GATE =
new DescribedPredicate<>("anything under " + SDLGPU_ROOT + " other than SDLGPUGate") {
@Override
public boolean test(JavaClass c) {
final String name = c.getName();
return name.startsWith(SDLGPU_ROOT + ".") && !GATE.equals(name);
}
};
private static final DescribedPredicate<JavaClass> LWJGL3_ONLY_TYPE =
new DescribedPredicate<>("types that only exist under lwjgl3ify/LWJGL3") {
@Override
public boolean test(JavaClass c) {
final String name = c.getName();
if (LWJGL3_AWARE.equals(name)) return false;
return name.startsWith("org.lwjglx.") || name.startsWith("me.eigenraven.lwjgl3ify.") || name.startsWith("org.lwjgl.sdl.");
}
};
private static final JavaClasses CLASSES = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPaths("build/classes/java/main", "build/classes/java/mixin", "glsm/build/classes/java/main");
private static final JavaClasses SDLGPU_CLASSES = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPaths("sdl-gpu/build/classes/java/main");
private static final ArchRule SDLGPU_REACHED_ONLY_THROUGH_THE_GATE = noClasses()
.that().resideOutsideOfPackage(SDLGPU_ROOT + "..")
.should().dependOnClassesThat(SDLGPU_BEHIND_THE_GATE)
.because("SDLGPUGate is the only :sdl-gpu class safe to load without LWJGL3");
private static final ArchRule NO_LWJGL3_ONLY_TYPES = noClasses()
.that().resideOutsideOfPackage(SDLGPU_ROOT + "..")
.should().dependOnClassesThat(LWJGL3_ONLY_TYPE)
.because("main, mixin and glsm all load on an LWJGL2 launch, where these types are absent");
@Test
void sdlGpuIsReachedOnlyThroughTheGate() {
SDLGPU_REACHED_ONLY_THROUGH_THE_GATE.check(CLASSES);
final List<String> violations = referencesOutsideSdlGpu(SDLGPUEncapsulationTest::isBehindTheGate);
assertTrue(violations.isEmpty(), "SDLGPUGate is the only :sdl-gpu class safe to load without LWJGL3:\n" + String.join("\n", violations));
}
@Test
void noLwjgl3OnlyTypesOutsideSdlGpu() {
NO_LWJGL3_ONLY_TYPES.check(CLASSES);
final List<String> violations = referencesOutsideSdlGpu(SDLGPUEncapsulationTest::isLwjgl3OnlyType);
assertTrue(violations.isEmpty(), "main, mixin and glsm all load on an LWJGL2 launch, where these types are absent:\n" + String.join("\n", violations));
}
@Test
void gateStaticInitIsLwjgl3Free() {
final Optional<JavaStaticInitializer> clinit = SDLGPU_CLASSES.get(GATE).getStaticInitializer();
assertTrue(clinit.isPresent(), GATE + " has no static initializer to check; the importer path is likely wrong");
final ScannedClass gate = ArchScan.get(GATE);
assertTrue(gate.methods().stream().anyMatch(m -> m.name().equals("<clinit>")), GATE + " has no static initializer to check; the scan path is likely wrong");
final List<String> violations = new ArrayList<>();
for (JavaAccess<?> access : clinit.get().getAccessesFromSelf()) {
if (isLwjgl3(access.getTargetOwner())) violations.add(" " + access.getTarget().getFullName());
for (Access access : gate.accesses()) {
if (isStaticInit(access.method()) && isLwjgl3(access.owner())) violations.add(" " + access.target());
}
for (ReferencedClassObject ref : clinit.get().getReferencedClassObjects()) {
if (isLwjgl3(ref.getValue())) violations.add(" " + ref.getValue().getName() + ".class");
for (ClassConstant constant : gate.classConstants()) {
if (isStaticInit(constant.method()) && isLwjgl3(constant.type())) violations.add(" " + constant.type() + ".class");
}
assertTrue(violations.isEmpty(), GATE + " static init resolves LWJGL3 types:\n" + String.join("\n", violations));
}
private static boolean isLwjgl3(JavaClass c) {
return LWJGL3_ONLY_TYPE.test(c) || c.getName().startsWith("org.lwjgl.");
private static List<String> referencesOutsideSdlGpu(Predicate<String> forbidden) {
final List<String> violations = new ArrayList<>();
for (ScannedClass origin : ArchScan.in(Dir.MAIN, Dir.MIXIN, Dir.GLSM)) {
if (origin.name().startsWith(SDLGPU_ROOT + ".")) continue;
for (String reference : origin.references()) {
if (forbidden.test(reference)) violations.add(" " + origin.name() + " -> " + reference);
}
}
Collections.sort(violations);
return violations;
}
private static boolean isBehindTheGate(String name) {
return name.startsWith(SDLGPU_ROOT + ".") && !GATE.equals(name);
}
private static boolean isLwjgl3OnlyType(String name) {
if (LWJGL3_AWARE.equals(name)) return false;
return name.startsWith("org.lwjglx.") || name.startsWith("me.eigenraven.lwjgl3ify.") || name.startsWith("org.lwjgl.sdl.");
}
private static boolean isLwjgl3(String name) {
return isLwjgl3OnlyType(name) || name.startsWith("org.lwjgl.");
}
private static boolean isStaticInit(String method) {
return method.startsWith("<clinit>") || method.startsWith("lambda$static$");
}
}
@@ -81,15 +81,19 @@ final class BopFogTestSupport {
}
}
static <T> T stub(Class<T> type) {
return Mockito.mock(type, Mockito.withSettings().stubOnly());
}
static World mockWorld(Grid grid) {
final World world = Mockito.mock(World.class);
final IChunkProvider provider = Mockito.mock(IChunkProvider.class);
final World world = stub(World.class);
final IChunkProvider provider = stub(IChunkProvider.class);
when(world.getChunkProvider()).thenReturn(provider);
when(provider.chunkExists(anyInt(), anyInt())).thenReturn(true);
final BiomeGenBase fogBiome = BopFog.mockBiome();
final BiomeGenBase fogBiome = Mockito.mock(BiomeGenBase.class, Mockito.withSettings().extraInterfaces(BopFog.TYPE).stubOnly());
when(BopFog.stubCall(fogBiome)).thenAnswer(inv -> grid.colorAt(inv.getArgument(0), inv.getArgument(1), inv.getArgument(2)));
final BiomeGenBase plainBiome = Mockito.mock(BiomeGenBase.class);
final BiomeGenBase plainBiome = stub(BiomeGenBase.class);
when(world.getBiomeGenForCoords(anyInt(), anyInt())).thenAnswer(inv -> grid.isFog(inv.getArgument(0), inv.getArgument(1)) ? fogBiome : plainBiome);
return world;
@@ -12,9 +12,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static com.gtnewhorizons.angelica.compat.bop.BopFogTestSupport.mockWorld;
import static com.gtnewhorizons.angelica.compat.bop.BopFogTestSupport.stub;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
@@ -26,8 +26,8 @@ class FogAggregateTest {
}
private static World world(AtomicBoolean loaded, AtomicInteger color) {
final World world = mock(World.class);
final IChunkProvider provider = mock(IChunkProvider.class);
final World world = stub(World.class);
final IChunkProvider provider = stub(IChunkProvider.class);
when(world.getChunkProvider()).thenReturn(provider);
when(provider.chunkExists(anyInt(), anyInt())).thenAnswer(inv -> loaded.get());
final BiomeGenBase biome = BopFog.mockBiome();
@@ -146,7 +146,7 @@ class FogAggregateTest {
final AtomicBoolean loaded = new AtomicBoolean(false);
final World world = world(loaded, new AtomicInteger(0x010203));
final BiomeGenBase fog = world.getBiomeGenForCoords(0, 0);
when(world.getBiomeGenForCoords(anyInt(), anyInt())).thenReturn(mock(BiomeGenBase.class));
when(world.getBiomeGenForCoords(anyInt(), anyInt())).thenReturn(stub(BiomeGenBase.class));
final float[] actual = new float[4];
FogColourGrid.accumulateInterior(world, 0, 64, 0, 1, actual);
assertBits(new float[4], actual);
@@ -5,12 +5,12 @@ import net.minecraft.world.biome.BiomeGenBase;
import net.minecraft.world.chunk.IChunkProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.function.IntBinaryOperator;
import com.gtnewhorizons.angelica.compat.bop.BopFogTestSupport.BopFog;
import static com.gtnewhorizons.angelica.compat.bop.BopFogTestSupport.stub;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.times;
@@ -25,8 +25,8 @@ class FogColourGridTest {
}
private static World mockWorld(boolean chunksLoaded) {
final World world = Mockito.mock(World.class);
final IChunkProvider provider = Mockito.mock(IChunkProvider.class);
final World world = stub(World.class);
final IChunkProvider provider = stub(IChunkProvider.class);
when(world.getChunkProvider()).thenReturn(provider);
when(provider.chunkExists(anyInt(), anyInt())).thenReturn(chunksLoaded);
return world;
@@ -0,0 +1,166 @@
package com.gtnewhorizons.angelica.rendering.celeritas;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.data.AnimationMetadataSection;
import org.embeddedt.embeddium.impl.model.quad.properties.ModelQuadFacing;
import org.embeddedt.embeddium.impl.render.chunk.compile.sorting.QuadPrimitiveType;
import org.embeddedt.embeddium.impl.render.chunk.sprite.SpriteTransparencyLevel;
import org.embeddedt.embeddium.impl.render.chunk.terrain.TerrainRenderPass;
import org.embeddedt.embeddium.impl.render.chunk.terrain.material.Material;
import org.embeddedt.embeddium.impl.render.chunk.terrain.material.parameters.AlphaCutoffParameter;
import org.embeddedt.embeddium.impl.render.chunk.vertex.format.ChunkMeshFormats;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertSame;
class QuadMaterialSelectorTest {
private final Material[] previous = new Material[3];
private final Map<TextureAtlasSprite, SpriteTransparencyLevel> levels = new IdentityHashMap<>();
private final QuadMaterialSelector selector = new QuadMaterialSelector() {
@Override
SpriteTransparencyLevel transparencyLevel(TextureAtlasSprite sprite) {
return levels.get(sprite);
}
};
private Material solid;
private Material cutout;
private Material translucent;
@BeforeEach
void installMaterials() {
previous[0] = AngelicaRenderPassConfiguration.SOLID_MATERIAL;
previous[1] = AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL;
previous[2] = AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL;
solid = new Material(pass("solid", false), AlphaCutoffParameter.ZERO, true);
cutout = new Material(pass("cutout_mipped", true), AlphaCutoffParameter.HALF, true);
translucent = new Material(pass("translucent", false), AlphaCutoffParameter.ZERO, true);
AngelicaRenderPassConfiguration.SOLID_MATERIAL = solid;
AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL = cutout;
AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL = translucent;
}
@AfterEach
void restoreMaterials() {
AngelicaRenderPassConfiguration.SOLID_MATERIAL = previous[0];
AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL = previous[1];
AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL = previous[2];
}
private static TerrainRenderPass pass(String name, boolean fragmentDiscard) {
return new TerrainRenderPass(name, null, false, fragmentDiscard, false, false, ChunkMeshFormats.VANILLA_LIKE, QuadPrimitiveType.TRIANGULATED, Map.of());
}
private TextureAtlasSprite sprite(String name, SpriteTransparencyLevel level) {
final TextureAtlasSprite sprite = new TextureAtlasSprite(name);
levels.put(sprite, level);
return sprite;
}
private TextureAtlasSprite animated(String name) {
final TextureAtlasSprite sprite = sprite(name, SpriteTransparencyLevel.OPAQUE);
sprite.animationMetadata = new AnimationMetadataSection(new ArrayList<>(), 16, 16, 1);
return sprite;
}
private TextureAtlasSprite custom(String name) {
final TextureAtlasSprite sprite = new TextureAtlasSprite(name) {};
levels.put(sprite, SpriteTransparencyLevel.OPAQUE);
return sprite;
}
private void begin(Material material) {
selector.reset(material, false, true);
}
private Material select(ModelQuadFacing facing, TextureAtlasSprite sprite) {
return selector.select(facing, sprite, false);
}
@Test
void opaqueOverlayStaysWithAnimatedBaseOnTheSameFace() {
final TextureAtlasSprite base = animated("hadesX32");
final TextureAtlasSprite corner = sprite("metalborder", SpriteTransparencyLevel.OPAQUE);
final TextureAtlasSprite edge = sprite("metalborder-ctm", SpriteTransparencyLevel.TRANSPARENT);
begin(cutout);
assertSame(cutout, select(ModelQuadFacing.POS_Z, base));
assertSame(cutout, select(ModelQuadFacing.POS_Z, corner));
assertSame(cutout, select(ModelQuadFacing.POS_Z, edge));
assertSame(cutout, select(ModelQuadFacing.POS_Z, corner));
}
@Test
void cachedDemotionDoesNotLeakOntoAFaceWithABase() {
final TextureAtlasSprite base = animated("hadesX32");
final TextureAtlasSprite corner = sprite("metalborder", SpriteTransparencyLevel.OPAQUE);
begin(cutout);
assertSame(solid, select(ModelQuadFacing.POS_X, corner));
assertSame(cutout, select(ModelQuadFacing.POS_Z, base));
assertSame(cutout, select(ModelQuadFacing.POS_Z, corner));
assertSame(solid, select(ModelQuadFacing.POS_X, corner));
}
@Test
void opaqueOnlyFacesAreStillDemoted() {
final TextureAtlasSprite stone = sprite("stone", SpriteTransparencyLevel.OPAQUE);
begin(cutout);
for (ModelQuadFacing facing : ModelQuadFacing.VALUES) {
assertSame(solid, select(facing, stone));
}
}
@Test
void vertexAlphaKeepsTranslucentAndPinsTheFace() {
final TextureAtlasSprite frame = sprite("frame", SpriteTransparencyLevel.TRANSPARENT);
begin(translucent);
assertSame(cutout, select(ModelQuadFacing.NEG_Y, frame));
assertSame(translucent, selector.select(ModelQuadFacing.POS_Y, frame, true));
assertSame(translucent, select(ModelQuadFacing.POS_Y, frame));
assertSame(cutout, select(ModelQuadFacing.NEG_Y, frame));
}
@Test
void undemotableSpritesKeepTheBaseMaterial() {
begin(cutout);
assertSame(cutout, select(ModelQuadFacing.POS_X, null));
assertSame(cutout, select(ModelQuadFacing.POS_Y, animated("animated")));
assertSame(cutout, select(ModelQuadFacing.POS_Z, custom("custom")));
}
@Test
void shaderPackOverrideAndDisabledOptimizationKeepTheBaseMaterial() {
final TextureAtlasSprite stone = sprite("stone", SpriteTransparencyLevel.OPAQUE);
selector.reset(cutout, true, true);
assertSame(cutout, select(ModelQuadFacing.POS_X, stone));
selector.reset(cutout, false, false);
assertSame(cutout, select(ModelQuadFacing.POS_X, stone));
}
@Test
void resetReleasesPinnedFacesAndTheSpriteCache() {
final TextureAtlasSprite base = animated("hadesX32");
final TextureAtlasSprite corner = sprite("metalborder", SpriteTransparencyLevel.OPAQUE);
begin(cutout);
assertSame(cutout, select(ModelQuadFacing.POS_Z, base));
assertSame(cutout, select(ModelQuadFacing.POS_Z, corner));
assertSame(solid, select(ModelQuadFacing.NEG_Z, corner));
begin(translucent);
assertSame(cutout, select(ModelQuadFacing.POS_Z, corner));
}
}
@@ -262,7 +262,7 @@ class SectionMetaBufferTest {
});
try {
Thread.sleep(2000);
Thread.sleep(200);
stop.set(true);
writer.join();
reader.join();
-2
View File
@@ -1,2 +0,0 @@
# The arch tests only need names and descriptors off the imported bytecode.
resolveMissingDependenciesFromClassPath=false