diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..09e0c8a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..119e010 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/Phosphophyllite b/Phosphophyllite index d1b37c8..24c67ee 160000 --- a/Phosphophyllite +++ b/Phosphophyllite @@ -1 +1 @@ -Subproject commit d1b37c8d53a79a897de0c219bc12411ea57d809a +Subproject commit 24c67ee51f099ef1fb182079854577e8ead6d027 diff --git a/build.gradle b/build.gradle index 0b96d99..ff94241 100644 --- a/build.gradle +++ b/build.gradle @@ -54,6 +54,7 @@ runs { systemProperty 'forge.logging.console.level', 'debug' modSource project.sourceSets.main + modSource project.sourceSets.test modSource project(':Phosphophyllite').sourceSets.main dependencies { @@ -94,6 +95,7 @@ dependencies { implementation "net.neoforged:neoforge:${neo_version}" compileOnly project(':Phosphophyllite') + testCompileOnly project(':Phosphophyllite') compileOnly("org.lwjgl:lwjgl-vulkan:3.3.1") { transitive(false) } diff --git a/runTests.py b/runTests.py new file mode 100644 index 0000000..ffe60cc --- /dev/null +++ b/runTests.py @@ -0,0 +1,44 @@ +import os +import shutil +import subprocess + +runDirectory = "run/client/" +configDirectory = runDirectory + "config/phosphophyllite/" + +runMode = os.environ.get("QUARTZ_TEST_RUN_MODE") +if runMode is None: + runMode = "Automatic" + pass + +mainConfigName = "quartz-client.json5" +mainConfig = f""" +{{ + debug: false, + mode: "{runMode}", +}} +""" + +testConfigName = "quartz-testing-client.json5" +testConfig = """ +{ + Enabled: true, + AutoRun: true, +} +""" + +if __name__ == '__main__': + print(mainConfig) + print(testConfig) + shutil.rmtree(runDirectory) + os.makedirs(configDirectory) + + mainConfigFile = open(configDirectory + mainConfigName, "w") + mainConfigFile.write(mainConfig) + mainConfigFile.close() + + testConfigFile = open(configDirectory + testConfigName, "w") + testConfigFile.write(testConfig) + testConfigFile.close() + + subprocess.call(["./gradlew", ":runClient"]) + pass diff --git a/settings.gradle b/settings.gradle index 6a5620f..48a23ce 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,4 +15,6 @@ plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0' } -include 'Phosphophyllite' \ No newline at end of file +include 'Phosphophyllite' + +startParameter.excludedTaskNames << ':Phosphophyllite:runClient' \ No newline at end of file diff --git a/src/main/java/net/roguelogix/quartz/QuartzConfig.java b/src/main/java/net/roguelogix/quartz/QuartzConfig.java index 9d3deaa..bfb26a4 100644 --- a/src/main/java/net/roguelogix/quartz/QuartzConfig.java +++ b/src/main/java/net/roguelogix/quartz/QuartzConfig.java @@ -42,7 +42,7 @@ private static boolean isValidPhosLoaading() { } private static boolean setup() { - if (!QuartzDebug.doesForgeExist()) { + if (!QuartzDebug.Util.doesForgeExist()) { // loading without forge present return false; } diff --git a/src/main/java/net/roguelogix/quartz/QuartzEvent.java b/src/main/java/net/roguelogix/quartz/QuartzEvent.java index fcfd31d..833ec1f 100644 --- a/src/main/java/net/roguelogix/quartz/QuartzEvent.java +++ b/src/main/java/net/roguelogix/quartz/QuartzEvent.java @@ -19,7 +19,8 @@ public static class ResourcesReloaded extends ResourcesLoaded { } public static class FrameStart extends QuartzEvent { - public FrameStart() { - } + } + + public static class FrameEnd extends QuartzEvent { } } diff --git a/src/main/java/net/roguelogix/quartz/internal/QuartzCore.java b/src/main/java/net/roguelogix/quartz/internal/QuartzCore.java index 2648a3a..c0b1c73 100644 --- a/src/main/java/net/roguelogix/quartz/internal/QuartzCore.java +++ b/src/main/java/net/roguelogix/quartz/internal/QuartzCore.java @@ -29,22 +29,51 @@ import net.roguelogix.quartz.internal.world.WorldEngine; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; import org.joml.Matrix4f; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.lang.ref.Cleaner; import java.util.List; -import static net.roguelogix.quartz.internal.QuartzDebug.doesForgeExist; - @ClientOnly @NonnullDefault public abstract class QuartzCore { public static final Logger LOGGER = LogManager.getLogger("Quartz"); - @Nonnull + // this should help the JIT with removing the code used for testing, and the branch testing that goes along with it + public static final boolean TESTING_ALLOWED; + + static { + boolean testingAllowed = false; + try { + final var testingClass = QuartzCore.class.getClassLoader().loadClass("net.roguelogix.quartz.testing.QuartzTestingConfig"); + final var instanceField = testingClass.getField("INSTANCE"); + final var enabledField = testingClass.getField("Enabled"); + final var isEnabled = enabledField.get(instanceField.get(null)); + testingAllowed = (Boolean)isEnabled; + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + TESTING_ALLOWED = testingAllowed; + } + + private static boolean TESTING_RUNNING = false; + + private static void onTestingStatusEvent(QuartzInternalEvent.TestingStatus testingStatus) { + if(!TESTING_ALLOWED){ + return; + } + TESTING_RUNNING = testingStatus.running; + } + + public static boolean isTestingRunning() { + if(!TESTING_ALLOWED){ + return false; + } + return TESTING_RUNNING; + } + public static final QuartzCore INSTANCE; public static final Cleaner CLEANER = Cleaner.create(); public static final WorkQueue deletionQueue = new WorkQueue(); @@ -54,8 +83,9 @@ public static void mainThreadClean(Object referent, Runnable cleanFunc) { } static { + @Nullable QuartzCore instance = null; - if (!DatagenModLoader.isRunningDataGen() && doesForgeExist()) { + if (!DatagenModLoader.isRunningDataGen() && QuartzDebug.Util.doesForgeExist()) { if (!Thread.currentThread().getStackTrace()[2].getClassName().equals(EventListener.class.getName())) { throw new IllegalStateException("Attempt to init quartz before it is ready"); } @@ -142,6 +172,7 @@ public static void onModelRegisterEvent(ModelEvent.RegisterAdditional event) { @OnModLoad private static void onModLoad() { + Quartz.EVENT_BUS.addListener(QuartzCore::onTestingStatusEvent); ModLoadingContext.get().getActiveContainer().getEventBus().addListener(QuartzCore::onModelRegisterEvent); } @@ -216,6 +247,8 @@ public DrawBatch getEntityBatcher() { public abstract void waitIdle(); + public abstract void fullSyncWait(); + public abstract int frameInFlight(); public abstract void sectionDirty(int x, int y, int z); diff --git a/src/main/java/net/roguelogix/quartz/internal/QuartzDebug.java b/src/main/java/net/roguelogix/quartz/internal/QuartzDebug.java index d380061..07ad266 100644 --- a/src/main/java/net/roguelogix/quartz/internal/QuartzDebug.java +++ b/src/main/java/net/roguelogix/quartz/internal/QuartzDebug.java @@ -8,26 +8,28 @@ public class QuartzDebug { public static final boolean DEBUG; static { - if (!doesForgeExist() || runningDatagen()) { + if (!Util.doesForgeExist() || Util.runningDatagen()) { DEBUG = false; } else { - DEBUG = QuartzConfig.INSTANCE.debug; + DEBUG = QuartzCore.TESTING_ALLOWED || QuartzConfig.INSTANCE.debug; } } - public static boolean runningDatagen(){ - try{ - return DatagenModLoader.isRunningDataGen(); - } catch (Throwable e){ - return false; + public static class Util { + public static boolean runningDatagen() { + try { + return DatagenModLoader.isRunningDataGen(); + } catch (Throwable e) { + return false; + } } - } - - public static boolean doesForgeExist(){ - try{ - return FMLLoader.getLoadingModList() != null; - } catch (Throwable e){ - return false; + + public static boolean doesForgeExist() { + try { + return FMLLoader.getLoadingModList() != null; + } catch (Throwable e) { + return false; + } } } } diff --git a/src/main/java/net/roguelogix/quartz/internal/QuartzInternalEvent.java b/src/main/java/net/roguelogix/quartz/internal/QuartzInternalEvent.java new file mode 100644 index 0000000..46c6dd5 --- /dev/null +++ b/src/main/java/net/roguelogix/quartz/internal/QuartzInternalEvent.java @@ -0,0 +1,36 @@ +package net.roguelogix.quartz.internal; + +import net.minecraft.client.renderer.RenderType; +import net.neoforged.bus.api.Event; +import net.roguelogix.phosphophyllite.util.NonnullDefault; +import net.roguelogix.phosphophyllite.util.Pair; +import net.roguelogix.quartz.internal.util.PointerWrapper; + +import java.util.List; +import java.util.Map; + +@NonnullDefault +public class QuartzInternalEvent extends Event { + public static class CreateTests extends QuartzInternalEvent { + } + + public static class TestingStatus extends QuartzInternalEvent { + public final boolean running; + + public TestingStatus(boolean running) { + this.running = running; + } + } + + public static class FeedbackCollected extends QuartzInternalEvent { + + public final List renderTypes; + // note: readOnly pointers + public final Map> feedbackBuffers; + + public FeedbackCollected(List renderTypes, Map> feedbackBuffers) { + this.renderTypes = renderTypes; + this.feedbackBuffers = feedbackBuffers; + } + } +} diff --git a/src/main/java/net/roguelogix/quartz/internal/gl33/GL33Core.java b/src/main/java/net/roguelogix/quartz/internal/gl33/GL33Core.java index b22cee8..a7a59cd 100644 --- a/src/main/java/net/roguelogix/quartz/internal/gl33/GL33Core.java +++ b/src/main/java/net/roguelogix/quartz/internal/gl33/GL33Core.java @@ -211,6 +211,11 @@ public void waitIdle() { // no need to wait for GPU idle } + @Override + public void fullSyncWait() { + // no syncing needed + } + @Override public int frameInFlight() { return 0; diff --git a/src/main/java/net/roguelogix/quartz/internal/gl46/GL46Core.java b/src/main/java/net/roguelogix/quartz/internal/gl46/GL46Core.java index 7218606..7fb2673 100644 --- a/src/main/java/net/roguelogix/quartz/internal/gl46/GL46Core.java +++ b/src/main/java/net/roguelogix/quartz/internal/gl46/GL46Core.java @@ -11,6 +11,8 @@ import net.minecraft.client.renderer.RenderType; import net.roguelogix.phosphophyllite.util.NonnullDefault; import net.roguelogix.quartz.DrawBatch; +import net.roguelogix.quartz.Quartz; +import net.roguelogix.quartz.QuartzEvent; import net.roguelogix.quartz.internal.Buffer; import net.roguelogix.quartz.internal.IrisDetection; import net.roguelogix.quartz.internal.QuartzCore; @@ -20,7 +22,7 @@ import java.util.List; -import static org.lwjgl.opengl.GL46C.glFinish; +import static org.lwjgl.opengl.GL45C.*; @NonnullDefault public class GL46Core extends QuartzCore { @@ -112,6 +114,8 @@ public void frameStart(PoseStack pMatrixStack, float pPartialTicks, long pFinish drawInfo.deltaNano = deltaNano; drawInfo.partialTicks = pPartialTicks; + Quartz.EVENT_BUS.post(new QuartzEvent.FrameStart()); + GL46FeedbackDrawing.beginFrame(); } @@ -184,7 +188,7 @@ public void endOpaque() { @Override public void endTranslucent() { - + Quartz.EVENT_BUS.post(new QuartzEvent.FrameEnd()); } @Override @@ -192,6 +196,12 @@ public void waitIdle() { glFinish(); } + @Override + public void fullSyncWait() { + glMemoryBarrier(GL_ALL_BARRIER_BITS); + glFinish(); + } + @Override public int frameInFlight() { return frameInFlight; diff --git a/src/main/java/net/roguelogix/quartz/internal/gl46/GL46FeedbackDrawing.java b/src/main/java/net/roguelogix/quartz/internal/gl46/GL46FeedbackDrawing.java index 24e9cad..74df861 100644 --- a/src/main/java/net/roguelogix/quartz/internal/gl46/GL46FeedbackDrawing.java +++ b/src/main/java/net/roguelogix/quartz/internal/gl46/GL46FeedbackDrawing.java @@ -3,10 +3,13 @@ import com.mojang.blaze3d.systems.RenderSystem; import it.unimi.dsi.fastutil.objects.*; import net.minecraft.client.renderer.RenderType; +import net.roguelogix.phosphophyllite.util.Pair; import net.roguelogix.quartz.DrawBatch; +import net.roguelogix.quartz.Quartz; import net.roguelogix.quartz.internal.*; import net.roguelogix.quartz.internal.common.B3DStateHelper; import net.roguelogix.quartz.internal.gl46.batching.GL46DrawBatch; +import net.roguelogix.quartz.internal.util.PointerWrapper; import net.roguelogix.quartz.internal.util.VertexFormatOutput; import org.joml.Matrix4f; @@ -46,8 +49,8 @@ public static void dirtyAll() { private record FeedbackBuffer(int buffer, int size) { private FeedbackBuffer(int size) { this(glCreateBuffers(), roundUpPo2(size)); - // no flags, only used on the server side - glNamedBufferStorage(buffer, this.size, 0); + // no flags, only used on the server side, unless testing + glNamedBufferStorage(buffer, this.size, QuartzCore.TESTING_ALLOWED ? GL_MAP_READ_BIT | GL_MAP_PERSISTENT_BIT : 0); } void delete() { @@ -63,7 +66,8 @@ private static int roundUpPo2(int minSize) { } } - private static final Object2ObjectMap renderTypeFeedbackBuffers = new Object2ObjectArrayMap<>(); + private static final Reference2ObjectMap renderTypeFeedbackBuffers = new Reference2ObjectArrayMap<>(); + private static final Reference2LongMap renderTypeFeedbackBufferMappings = new Reference2LongArrayMap<>(); private static Buffer.CallbackHandle rebuildCallbackHandle; @@ -274,6 +278,14 @@ public static void collectAllFeedback(boolean shadowsEnabled) { drawBatch.setFrameSync(frameSync); } prevousFrameSyncs[frameInFlight] = frameSync; + + if(QuartzCore.TESTING_ALLOWED && QuartzCore.isTestingRunning()){ + var buffers = new Object2ObjectOpenHashMap>(); + for (RenderType renderType : inUseRenderTypes) { + buffers.put(renderType, new Pair<>(null, 0)); + } + Quartz.EVENT_BUS.post(new QuartzInternalEvent.FeedbackCollected(inUseRenderTypes, buffers)); + } } private static Matrix4f projection; diff --git a/src/main/java/net/roguelogix/quartz/internal/vk/VKCore.java b/src/main/java/net/roguelogix/quartz/internal/vk/VKCore.java index 68164bc..5f6df15 100644 --- a/src/main/java/net/roguelogix/quartz/internal/vk/VKCore.java +++ b/src/main/java/net/roguelogix/quartz/internal/vk/VKCore.java @@ -93,6 +93,11 @@ public void waitIdle() { } + @Override + public void fullSyncWait() { + + } + @Override public int frameInFlight() { return 0; diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index de35715..6408f96 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -61,4 +61,9 @@ public net.minecraft.client.renderer.RenderStateShard NO_TEXTURE public net.minecraft.client.renderer.RenderStateShard NO_TRANSPARENCY -public net.minecraft.client.renderer.LightTexture lightTexture \ No newline at end of file +public net.minecraft.client.renderer.LightTexture lightTexture + +public com.mojang.blaze3d.platform.GlDebug printDebugLog(IIIIIJJ)V +public com.mojang.blaze3d.platform.NativeImage pixels +public net.minecraft.client.Camera setPosition(DDD)V +public net.minecraft.client.Camera setRotation(FF)V \ No newline at end of file diff --git a/src/test/java/net/roguelogix/quartz/testing/AutomaticTesting.java b/src/test/java/net/roguelogix/quartz/testing/AutomaticTesting.java new file mode 100644 index 0000000..c313252 --- /dev/null +++ b/src/test/java/net/roguelogix/quartz/testing/AutomaticTesting.java @@ -0,0 +1,110 @@ +package net.roguelogix.quartz.testing; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.JsonOps; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.GenericDirtMessageScreen; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.RegistryOps; +import net.minecraft.world.Difficulty; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.LevelSettings; +import net.minecraft.world.level.WorldDataConfiguration; +import net.minecraft.world.level.levelgen.FlatLevelSource; +import net.minecraft.world.level.levelgen.WorldDimensions; +import net.minecraft.world.level.levelgen.WorldOptions; +import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; +import net.minecraft.world.level.levelgen.presets.WorldPresets; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.ScreenEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.roguelogix.phosphophyllite.registry.OnModLoad; +import net.roguelogix.phosphophyllite.threading.Queues; +import net.roguelogix.quartz.Quartz; +import net.roguelogix.quartz.internal.QuartzCore; +import net.roguelogix.quartz.internal.QuartzInternalEvent; + +import java.util.Optional; +import java.util.function.Function; + +public class AutomaticTesting { + @OnModLoad + private static void onModLoad() { + if (!QuartzTestingConfig.INSTANCE.AutoRun) { + return; + } + NeoForge.EVENT_BUS.addListener(AutomaticTesting::onStartupEvent); + NeoForge.EVENT_BUS.addListener(AutomaticTesting::onLogin); + Quartz.EVENT_BUS.addListener(AutomaticTesting::onTestingStatus); + } + + private static boolean recursing = false; + + private static void onStartupEvent(ScreenEvent.Opening event) { + if (recursing) { + return; + } + recursing = true; + final var gameRules = new GameRules(); + gameRules.getRule(GameRules.RULE_DAYLIGHT).set(false, null); + gameRules.getRule(GameRules.RULE_WEATHER_CYCLE).set(false, null); + gameRules.getRule(GameRules.RULE_DO_PATROL_SPAWNING).set(false, null); + gameRules.getRule(GameRules.RULE_DO_TRADER_SPAWNING).set(false, null); + gameRules.getRule(GameRules.RULE_DO_WARDEN_SPAWNING).set(false, null); + gameRules.getRule(GameRules.RULE_DOBLOCKDROPS).set(false, null); + gameRules.getRule(GameRules.RULE_DO_VINES_SPREAD).set(false, null); + + final var levelSettings = new LevelSettings("QuartzAutomaticTesting", GameType.CREATIVE, false, Difficulty.PEACEFUL, true, gameRules, WorldDataConfiguration.DEFAULT); + + Function dimensionFunc = registryAccess -> { + + var generatorSettings = JsonParser.parseString(""" + { + "biome": "minecraft:the_void", + "features": true, + "lakes": false, + "layers": [ + { + "block": "minecraft:air", + "height": 1 + } + ], + "structure_overrides": [] + } + """); + RegistryOps registryops = RegistryOps.create(JsonOps.INSTANCE, registryAccess); + Optional optional = FlatLevelGeneratorSettings.CODEC + .parse(new Dynamic<>(registryops, generatorSettings)) + .resultOrPartial(QuartzCore.LOGGER::error); + + var worldDimensions = WorldPresets.createNormalWorldDimensions(registryAccess); + return worldDimensions.replaceOverworldGenerator(registryAccess, new FlatLevelSource(optional.get())); + }; + + Minecraft.getInstance().createWorldOpenFlows().createFreshLevel("QuartzAutomaticTesting", levelSettings, WorldOptions.defaultWithRandomSeed(), dimensionFunc, new GenericDirtMessageScreen(Component.translatable("selectWorld.data_read"))); + } + + public static void onLogin(ClientPlayerNetworkEvent.LoggingIn event) { + Queues.offThread.enqueueUntracked(() -> { + System.out.println("Testing starting in 3 seconds"); + // wait a little bit to start the tests, make sure everything has loaded in fully + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + Queues.clientThread.enqueue(QuartzTestRegistry::runAllTests); + }); + } + + public static void onTestingStatus(QuartzInternalEvent.TestingStatus event) { + if (!event.running) { + System.out.println("Testing completed, goodbye"); + System.exit(0); + } + } +} diff --git a/src/test/java/net/roguelogix/quartz/testing/QuartzTest.java b/src/test/java/net/roguelogix/quartz/testing/QuartzTest.java new file mode 100644 index 0000000..8d72cdb --- /dev/null +++ b/src/test/java/net/roguelogix/quartz/testing/QuartzTest.java @@ -0,0 +1,47 @@ +package net.roguelogix.quartz.testing; + +import net.roguelogix.phosphophyllite.util.NonnullDefault; +import net.roguelogix.quartz.internal.QuartzInternalEvent; + +import static net.roguelogix.quartz.testing.tests.Util.sendChatMessage; + +@NonnullDefault +public abstract class QuartzTest { + + public final String id; + + protected QuartzTest(String id) { + this.id = id.intern(); + } + + public abstract void setup(); + + public abstract void cleanup(); + + public abstract boolean running(); + + public abstract void frameStart(); + + public abstract void frameEnd(); + + public abstract void feedbackCollected(QuartzInternalEvent.FeedbackCollected event); + + private boolean passed = true; + + public final boolean passed(){ + return passed; + } + + public final void reset() { + passed = true; + } + + protected final void message(String message) { + sendChatMessage(message); + } + + protected final void fail(String message) { + sendChatMessage(message); + passed = false; + } +} diff --git a/src/test/java/net/roguelogix/quartz/testing/QuartzTestRegistry.java b/src/test/java/net/roguelogix/quartz/testing/QuartzTestRegistry.java new file mode 100644 index 0000000..3173848 --- /dev/null +++ b/src/test/java/net/roguelogix/quartz/testing/QuartzTestRegistry.java @@ -0,0 +1,116 @@ +package net.roguelogix.quartz.testing; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceArrayList; +import net.minecraft.CrashReport; +import net.minecraft.client.Minecraft; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.neoforged.neoforge.client.event.RegisterClientCommandsEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.roguelogix.phosphophyllite.registry.OnModLoad; +import net.roguelogix.quartz.Quartz; +import net.roguelogix.quartz.internal.QuartzCore; +import net.roguelogix.quartz.internal.QuartzInternalEvent; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +import static net.roguelogix.quartz.testing.QuartzTesting.AUTOMATED; +import static net.roguelogix.quartz.testing.tests.Util.*; + +public class QuartzTestRegistry { + + @OnModLoad + public static void onModLoad() { + NeoForge.EVENT_BUS.addListener(QuartzTestRegistry::registerCommands); + Quartz.EVENT_BUS.addListener(QuartzTestRegistry::onTestingStatusEvent); + registerTests(); + } + + public static void registerCommands(RegisterClientCommandsEvent event) { + event.getDispatcher().register(Commands.literal("quartz").then(Commands.literal("recreate_tests").executes(ctx -> { + registerTests(); + sendChatMessage("Tests recreated " + testRegistry.size()); + return Command.SINGLE_SUCCESS; + }))); + event.getDispatcher().register(Commands.literal("quartz").then(Commands.literal("run_all_tests").executes(ctx -> { + runAllTests(); + return Command.SINGLE_SUCCESS; + }))); + event.getDispatcher().register(Commands.literal("quartz").then(Commands.literal("run_test").then(Commands.argument("test_name", StringArgumentType.string()).executes(ctx -> { + runSpecificTest(ctx.getArgument("test_name", String.class)); + return Command.SINGLE_SUCCESS; + })))); + } + + public static void registerTests() { + testRegistry.clear(); + Quartz.EVENT_BUS.post(new QuartzInternalEvent.CreateTests()); + QuartzCore.resourcesReloaded(); + } + + public static void runAllTests() { + Minecraft.getInstance().player.sendSystemMessage(Component.literal("Quartz running all tests")); + QuartzTesting.runTests(testRegistry.values()); + } + + public static void runSpecificTest(String testName) { + @Nullable final var test = testRegistry.get(testName.intern()); + if (test == null) { + Minecraft.getInstance().player.sendSystemMessage(Component.literal("Quartz test " + testName + " not found")); + } else { + Minecraft.getInstance().player.sendSystemMessage(Component.literal("Quartz running test " + testName)); + QuartzTesting.runTests(List.of(test)); + } + } + + private static void onTestingStatusEvent(QuartzInternalEvent.TestingStatus testingStatus) { + if (testingStatus.running) { + failedTests.clear(); + savePlayerState(); + System.out.println("##teamcity[testSuiteStarted name='Quartz']"); + } else { + System.out.println("##teamcity[testSuiteFinished name='Quartz']"); + restorePlayerState(); + sendChatMessage(failedTests.size() + " tests failed"); + for (QuartzTest failedTest : failedTests) { + sendChatMessage(failedTest.id); + } + } + } + + private static final Reference2ReferenceMap testRegistry = new Reference2ReferenceOpenHashMap<>(); + private static final ReferenceArrayList failedTests = new ReferenceArrayList<>(); + + public static void registerTest(QuartzTest test, boolean runnable) { + testRegistry.put(test.id, test); + } + + public static void testStarted(QuartzTest test) { + sendChatMessage("Test " + test.id + " started"); + if (AUTOMATED) { + System.out.println("##teamcity[testStarted name='" + test.id + "']"); + } + } + + public static void testCompleted(QuartzTest test) { + if (!test.passed()) { + failedTests.add(test); + if (AUTOMATED) { + System.out.println("##teamcity[testFailed name='" + test.id + "' message='']"); + } + } + sendChatMessage("Test " + test.id + " " + (test.passed() ? "passed" : "failed")); + if (AUTOMATED) { + System.out.println("##teamcity[testFinished name='" + test.id + "']"); + } + } + + public static void fatalTestFailure(String message) { + Minecraft.getInstance().emergencySaveAndCrash(new CrashReport("Quartz testing caught fatal failure", new IllegalStateException(message))); + } +} diff --git a/src/test/java/net/roguelogix/quartz/testing/QuartzTesting.java b/src/test/java/net/roguelogix/quartz/testing/QuartzTesting.java new file mode 100644 index 0000000..5393c75 --- /dev/null +++ b/src/test/java/net/roguelogix/quartz/testing/QuartzTesting.java @@ -0,0 +1,144 @@ +package net.roguelogix.quartz.testing; + +import com.mojang.blaze3d.platform.DebugMemoryUntracker; +import com.mojang.blaze3d.platform.GLX; +import com.mojang.blaze3d.platform.GlDebug; +import it.unimi.dsi.fastutil.objects.ReferenceArrayList; +import net.minecraft.CrashReport; +import net.minecraft.client.Minecraft; +import net.neoforged.fml.ModLoadingContext; +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.roguelogix.phosphophyllite.registry.OnModLoad; +import net.roguelogix.quartz.Quartz; +import net.roguelogix.quartz.QuartzEvent; +import net.roguelogix.quartz.internal.QuartzInternalEvent; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GLDebugMessageCallback; +import org.lwjgl.opengl.GLDebugMessageCallbackI; +import org.lwjgl.opengl.KHRDebug; + +import java.util.Collection; + +import static net.roguelogix.quartz.testing.tests.Util.sendChatMessage; +import static org.lwjgl.opengl.GL46C.*; + +public class QuartzTesting { + + public static boolean TESTING_RUNNING = false; + public static final boolean AUTOMATED = QuartzTestingConfig.INSTANCE.AutoRun; + + @OnModLoad + private static void onModLoad() { + if (!QuartzTestingConfig.INSTANCE.Enabled) { + return; + } + Quartz.EVENT_BUS.addListener(QuartzTesting::onTestingStatusEvent); + Quartz.EVENT_BUS.addListener(QuartzTesting::onFrameStart); + Quartz.EVENT_BUS.addListener(QuartzTesting::onFrameEnd); + Quartz.EVENT_BUS.addListener(QuartzTesting::onFeedbackCollected); + ModLoadingContext.get().getActiveContainer().getEventBus().addListener(QuartzTesting::clientSetupEvent); + } + + private static void onTestingStatusEvent(QuartzInternalEvent.TestingStatus testingStatus) { + TESTING_RUNNING = testingStatus.running; + sendChatMessage("Quartz testing " + (TESTING_RUNNING ? "started" : "finished")); + } + + private static int nextTestIndex = 0; + private static final ReferenceArrayList runningTests = new ReferenceArrayList<>(); + @Nullable + private static QuartzTest activeTest = null; + + @Nullable + public static QuartzTest runningTest() { + return activeTest; + } + + private static void clientSetupEvent(FMLClientSetupEvent setupEvent) { + setupEvent.enqueueWork(() -> { + var glCaps = GL.getCapabilities(); + + if (!glCaps.GL_KHR_debug) { + Minecraft.getInstance().emergencySaveAndCrash(new CrashReport("Fatal test failure", new IllegalStateException("GL_KHR_debug required to run Quartz tests"))); + } + + // inject into the debug callback to fatal fail on an error + var myCallback = new GLDebugMessageCallbackI() { + @Override + public void invoke(int source, int type, int id, int severity, int length, long message, long userParam) { + GlDebug.printDebugLog(source, type, id, severity, length, message, userParam); + if (source == GL_DEBUG_SOURCE_APPLICATION) { + throw new RuntimeException("Application GL error caught"); + } + switch (type) { + case GL_DEBUG_TYPE_ERROR: + case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: + case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: { + QuartzTestRegistry.fatalTestFailure(GLDebugMessageCallback.getMessage(length, message)); + } + case GL_DEBUG_TYPE_PORTABILITY: { + + } + } + } + }; + + glEnable(GL_DEBUG_OUTPUT); + glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); + KHRDebug.glDebugMessageCallback(GLX.make(GLDebugMessageCallback.create(myCallback), DebugMemoryUntracker::untrack), 0L); + }); + } + + public static void runTests(Collection testList) { + if (TESTING_RUNNING) { + throw new IllegalStateException("Testing already running"); + } + runningTests.clear(); + runningTests.addAll(testList); + nextTestIndex = 0; + Quartz.EVENT_BUS.post(new QuartzInternalEvent.TestingStatus(true)); + } + + private static void onFrameStart(QuartzEvent.FrameStart frameStart) { + if (!TESTING_RUNNING) { + return; + } + if (activeTest == null) { + if (nextTestIndex < runningTests.size()) { + activeTest = runningTests.get(nextTestIndex); + QuartzTestRegistry.testStarted(activeTest); + activeTest.reset(); + activeTest.setup(); + nextTestIndex++; + } else { + Quartz.EVENT_BUS.post(new QuartzInternalEvent.TestingStatus(false)); + } + } + if (activeTest == null) { + return; + } + activeTest.frameStart(); + } + + private static void onFeedbackCollected(QuartzInternalEvent.FeedbackCollected feedbackEvent) { + if (activeTest == null) { + return; + } + activeTest.feedbackCollected(feedbackEvent); + } + + private static void onFrameEnd(QuartzEvent.FrameEnd frameEnd) { + if (activeTest == null) { + return; + } + activeTest.frameEnd(); + if (activeTest.running()) { + return; + } + activeTest.cleanup(); + QuartzTestRegistry.testCompleted(activeTest); + activeTest = null; + } +} diff --git a/src/test/java/net/roguelogix/quartz/testing/QuartzTestingConfig.java b/src/test/java/net/roguelogix/quartz/testing/QuartzTestingConfig.java new file mode 100644 index 0000000..7de216c --- /dev/null +++ b/src/test/java/net/roguelogix/quartz/testing/QuartzTestingConfig.java @@ -0,0 +1,37 @@ +package net.roguelogix.quartz.testing; + +import net.roguelogix.phosphophyllite.Phosphophyllite; +import net.roguelogix.phosphophyllite.config.ConfigManager; +import net.roguelogix.phosphophyllite.config.ConfigType; +import net.roguelogix.phosphophyllite.config.ConfigValue; +import net.roguelogix.phosphophyllite.registry.IgnoreRegistration; +import net.roguelogix.phosphophyllite.registry.RegisterConfig; +import net.roguelogix.quartz.QuartzConfig; + +public class QuartzTestingConfig { + @IgnoreRegistration + @RegisterConfig(folder = Phosphophyllite.modid, name = "quartz-testing", type = ConfigType.CLIENT) + public static final QuartzTestingConfig INSTANCE = new QuartzTestingConfig(); + + static { + if (QuartzConfig.INIT_COMPLETED) { + try { + // this needs to be registered extra extra early, so it can be read at quartz init + ConfigManager.registerConfig(INSTANCE, "blockstates/quartz", QuartzTestingConfig.class.getField("INSTANCE").getAnnotation(RegisterConfig.class)); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + throw new IllegalStateException(); + } + } + } + + @ConfigValue + public final boolean Enabled; + @ConfigValue + public final boolean AutoRun; + + { + Enabled = false; + AutoRun = false; + } +} diff --git a/src/test/java/net/roguelogix/quartz/testing/block/QuartzTestBlock.java b/src/test/java/net/roguelogix/quartz/testing/block/QuartzTestBlock.java new file mode 100644 index 0000000..f35c365 --- /dev/null +++ b/src/test/java/net/roguelogix/quartz/testing/block/QuartzTestBlock.java @@ -0,0 +1,49 @@ +package net.roguelogix.quartz.testing.block; + +import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.roguelogix.phosphophyllite.registry.CreativeTabBlock; +import net.roguelogix.phosphophyllite.registry.RegisterBlock; + +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +public class QuartzTestBlock extends Block implements EntityBlock { + + @CreativeTabBlock + @RegisterBlock(name = "quartz_test_runner_block", tileEntityClass = QuartzTestBlockTile.class) + public static final QuartzTestBlock INSTANCE = new QuartzTestBlock(); + + public QuartzTestBlock() { + super(Properties.of().noLootTable().destroyTime(3.0F).explosionResistance(3.0F)); + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pPos, BlockState pState) { + return QuartzTestBlockTile.SUPPLIER.create(pPos, pState); + } + + @Override + public InteractionResult use(BlockState state, Level worldIn, BlockPos pos, Player player, InteractionHand handIn, BlockHitResult hit) { + final var item = player.getMainHandItem().getItem(); + final var te = worldIn.getBlockEntity(pos); + if (te instanceof QuartzTestBlockTile tile && item instanceof BlockItem blockItem) { + tile.setBlock(blockItem.getBlock()); + return InteractionResult.SUCCESS; + } + return super.use(state, worldIn, pos, player, handIn, hit); + } +} diff --git a/src/test/java/net/roguelogix/quartz/testing/block/QuartzTestBlockTile.java b/src/test/java/net/roguelogix/quartz/testing/block/QuartzTestBlockTile.java new file mode 100644 index 0000000..6b4fa53 --- /dev/null +++ b/src/test/java/net/roguelogix/quartz/testing/block/QuartzTestBlockTile.java @@ -0,0 +1,65 @@ +package net.roguelogix.quartz.testing.block; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.roguelogix.phosphophyllite.modular.tile.PhosphophylliteTile; +import net.roguelogix.phosphophyllite.registry.RegisterTile; +import net.roguelogix.phosphophyllite.util.NonnullDefault; +import net.roguelogix.quartz.DrawBatch; +import net.roguelogix.quartz.Mesh; +import net.roguelogix.quartz.Quartz; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector3i; + +@NonnullDefault +public class QuartzTestBlockTile extends PhosphophylliteTile { + + @RegisterTile("quartz_test_runner_block") + public static final BlockEntityType.BlockEntitySupplier SUPPLIER = new RegisterTile.Producer<>(QuartzTestBlockTile::new); + + public QuartzTestBlockTile(BlockEntityType TYPE, BlockPos pWorldPosition, BlockState pBlockState) { + super(TYPE, pWorldPosition, pBlockState); + } + + @Nullable + private Mesh mesh; + @Nullable + private DrawBatch.Instance instance = null; + + @Override + public void onAdded() { + assert level != null; + if (!level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 0); + return; + } + if (mesh != null && level.isClientSide()) { + final var modelPos = new Vector3i(getBlockPos().getX(), getBlockPos().getY() + 2, getBlockPos().getZ()); + final var batcher = Quartz.getDrawBatcherForBlock(modelPos); + instance = batcher.createInstance(modelPos, mesh, null, null, null); + } + } + + @Override + public void onRemoved(boolean chunkUnload) { + assert level != null; + if (mesh != null && level.isClientSide()) { + if (instance != null) { + instance.delete(); + } + instance = null; + } + } + + void setBlock(Block block) { + assert level != null; + if(level.isClientSide()) { + onRemoved(false); + mesh = Quartz.createStaticMesh(block.defaultBlockState()); + mesh.rebuild(); + onAdded(); + } + } +} diff --git a/src/test/java/net/roguelogix/quartz/testing/events/TestingEvent.java b/src/test/java/net/roguelogix/quartz/testing/events/TestingEvent.java new file mode 100644 index 0000000..baa57d1 --- /dev/null +++ b/src/test/java/net/roguelogix/quartz/testing/events/TestingEvent.java @@ -0,0 +1,15 @@ +package net.roguelogix.quartz.testing.events; + +public class TestingEvent { + public static class Started extends TestingEvent { + } + + public static class Stopped extends TestingEvent { + } + + public static class FrameStart extends TestingEvent { + } + + public static class FrameEnd extends TestingEvent { + } +} diff --git a/src/test/java/net/roguelogix/quartz/testing/package-info.java b/src/test/java/net/roguelogix/quartz/testing/package-info.java new file mode 100644 index 0000000..f7bcd78 --- /dev/null +++ b/src/test/java/net/roguelogix/quartz/testing/package-info.java @@ -0,0 +1,7 @@ +// this package is dev mode only +@ClientOnly +@IgnoreRegistration(ignoreInDev = false) +package net.roguelogix.quartz.testing; + +import net.roguelogix.phosphophyllite.registry.ClientOnly; +import net.roguelogix.phosphophyllite.registry.IgnoreRegistration; \ No newline at end of file diff --git a/src/test/java/net/roguelogix/quartz/testing/tests/Util.java b/src/test/java/net/roguelogix/quartz/testing/tests/Util.java new file mode 100644 index 0000000..f7ef8c9 --- /dev/null +++ b/src/test/java/net/roguelogix/quartz/testing/tests/Util.java @@ -0,0 +1,197 @@ +package net.roguelogix.quartz.testing.tests; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Screenshot; +import net.minecraft.commands.arguments.EntityAnchorArgument; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.client.event.ViewportEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.roguelogix.phosphophyllite.registry.OnModLoad; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector3f; +import org.joml.Vector3ic; +import org.lwjgl.glfw.GLFW; + +public final class Util { + private static final Minecraft minecraft = Minecraft.getInstance(); + + private Util() { + } + + @OnModLoad + public static void onModLoadFOV(){ + NeoForge.EVENT_BUS.register(Util.class); + } + + @Nullable + private static Vec3 savedPosition; + private static float savedXRot; + private static float savedYRot; + private static int savedFOV; + private static boolean savedHUDHidden; + private static boolean savedFlying; + private static boolean savedBob; + private static ResourceKey savedLevelID; + private static int windowWidth; + private static int windowHeight; + + public static void savePlayerState() { + assert minecraft.player != null; + savedPosition = minecraft.player.position(); + savedXRot = minecraft.player.getXRot(); + savedYRot = minecraft.player.getYRot(); + savedFOV = minecraft.options.fov().get(); + savedHUDHidden = minecraft.options.hideGui; + savedFlying = minecraft.player.getAbilities().flying; + savedLevelID = minecraft.player.level().dimension(); + savedBob = minecraft.options.bobView().get(); + windowWidth = minecraft.getWindow().getWidth(); + windowHeight = minecraft.getWindow().getHeight(); + minecraft.getWindow().updateVsync(false); + } + + public static void restorePlayerState() { + assert minecraft.player != null; + minecraft.options.fov().set(savedFOV); + minecraft.options.hideGui = savedHUDHidden; + minecraft.player.getAbilities().flying = savedFlying; + minecraft.options.bobView().set(savedBob); + + var server = minecraft.getSingleplayerServer(); + assert server != null; + final var serverPlayer = server.getPlayerList().getPlayer(minecraft.player.getUUID()); + assert serverPlayer != null; + var level = server.getLevel(savedLevelID); + assert level != null; + assert savedPosition != null; + serverPlayer.teleportTo(level, savedPosition.x, savedPosition.y, savedPosition.z, savedXRot, savedYRot); + minecraft.player.setPos(savedPosition.x, savedPosition.y, savedPosition.z); + minecraft.player.setXRot(savedXRot); + minecraft.player.setXRot(savedYRot); + setWindowSize(windowWidth, windowHeight); + minecraft.getWindow().updateVsync(true); + } + + public static void hideHUD() { + minecraft.options.hideGui = true; + } + + public static void disableBob() { + minecraft.options.bobView().set(false); + } + + public static void setFOV(int fov) { + minecraft.options.fov().set(fov); + } + + @SubscribeEvent + public static void onFOVEvent(ViewportEvent.ComputeFov event){ + event.setFOV(minecraft.options.fov().get()); + } + + public static void setFlying() { + assert minecraft.player != null; + minecraft.player.getAbilities().flying = true; + } + + public static void setWindowSize(int w, int h) { + final var window = minecraft.getWindow().getWindow(); + GLFW.glfwRestoreWindow(window); + GLFW.glfwSetWindowSize(window, w, h); + } + +// public static void setEyePosAndLook(Vector3ic pos, Vector3ic look) { +// +// } + + public static void setEyePos(Vector3ic pos) { + setEyePos(pos.x(), pos.y(), pos.z()); + } + + public static void setEyePos(int x, int y, int z) { +// minecraft.gameRenderer.getMainCamera().setPosition(x + 0.5, y + 0.5, z + 0.5); + minecraft.player.absMoveTo(x + 0.5, y - minecraft.player.getEyeHeight() + 0.5, z + 0.5); + } + + public static void setLookCenter(int x, int y, int z) { + final var camera = minecraft.gameRenderer.getMainCamera(); + final var cameraPosition = camera.getPosition(); + final var lookPosition = new Vec3(x + 0.5, y + 0.5, z + 0.5); + + double diffX = lookPosition.x - cameraPosition.x; + double diffY = lookPosition.y - cameraPosition.y; + double diffZ = lookPosition.z - cameraPosition.z; + double XZDiffDot = Math.sqrt(diffX * diffX + diffZ * diffZ); + final var xRot = Mth.wrapDegrees((float)(-(Mth.atan2(diffY, XZDiffDot) * 180.0F / (float)Math.PI))); + final var yRot = Mth.wrapDegrees((float)(Mth.atan2(diffZ, diffX) * 180.0F / (float)Math.PI) - 90.0F); +// minecraft.gameRenderer.getMainCamera().setRotation(xRot, yRot); + minecraft.player.lookAt(EntityAnchorArgument.Anchor.EYES, new Vec3(x + 0.5, y + 0.5, z + 0.5)); + } + + public static void teleportToInOverworld(int x, int y, int z) { + var server = minecraft.getSingleplayerServer(); + assert server != null; + assert minecraft.player != null; + final var serverPlayer = server.getPlayerList().getPlayer(minecraft.player.getUUID()); + assert serverPlayer != null; + var level = server.getLevel(Level.OVERWORLD); + assert level != null; + serverPlayer.teleportTo(level, x + 0.5, z + 0.5, z + 0.5, 0, 0); + } + + public static void setBlock(int x, int y, int z, Block block) { + setBlock(new BlockPos(x, y, z), block); + } + + public static void setBlock(BlockPos pos, Block block) { + setBlock(pos, block.defaultBlockState()); + } + + public static void setBlock(int x, int y, int z, BlockState blockState) { + setBlock(new BlockPos(x, y, z), blockState); + } + + public static void setBlock(BlockPos pos, BlockState blockState) { + var player = minecraft.player; + assert player != null; + player.level().setBlockAndUpdate(pos, blockState); + } + + public static void setVolume(BlockPos starting, BlockPos ending, Block block) { + setVolume(starting, ending, block.defaultBlockState()); + } + + public static void setVolume(BlockPos starting, BlockPos ending, BlockState blockState) { + final var mutable = starting.mutable(); + for (int i = starting.getX(); i <= ending.getX(); i++) { + for (int j = starting.getY(); j < ending.getY(); j++) { + for (int k = starting.getZ(); k < ending.getZ(); k++) { + mutable.set(i, j, k); + setBlock(mutable, blockState); + } + } + } + } + + public static NativeImage screenshot() { + if (false) { + Screenshot.grab(minecraft.gameDirectory, minecraft.getMainRenderTarget(), p_90917_ -> minecraft.execute(() -> minecraft.gui.getChat().addMessage(p_90917_))); + } + return Screenshot.takeScreenshot(minecraft.getMainRenderTarget()); + } + + public static void sendChatMessage(String message) { + assert minecraft.player != null; + minecraft.player.sendSystemMessage(Component.literal(message)); + + } +} diff --git a/src/test/java/net/roguelogix/quartz/testing/tests/common/BlockMimicryTest.java b/src/test/java/net/roguelogix/quartz/testing/tests/common/BlockMimicryTest.java new file mode 100644 index 0000000..3c088d4 --- /dev/null +++ b/src/test/java/net/roguelogix/quartz/testing/tests/common/BlockMimicryTest.java @@ -0,0 +1,128 @@ +package net.roguelogix.quartz.testing.tests.common; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.Mod; +import net.roguelogix.phosphophyllite.registry.OnModLoad; +import net.roguelogix.phosphophyllite.util.NonnullDefault; +import net.roguelogix.quartz.DrawBatch; +import net.roguelogix.quartz.Mesh; +import net.roguelogix.quartz.Quartz; +import net.roguelogix.quartz.internal.QuartzInternalEvent; +import net.roguelogix.quartz.testing.tests.Util; +import org.joml.Vector3i; + +import javax.annotation.Nullable; + +import static net.roguelogix.quartz.testing.QuartzTestRegistry.registerTest; + + +@NonnullDefault +public class BlockMimicryTest extends ScreenshotCompareTest { + + private Block block; + private Mesh mesh; + @Nullable + private DrawBatch.Instance instance; + + public BlockMimicryTest(Block block) { + super("block_mimicry{" + BuiltInRegistries.BLOCK.getKey(block) + "}", new Vector3i(1, 1, 0)); + this.block = block; + mesh = Quartz.createStaticMesh(block.defaultBlockState()); + } + + @Override + protected void setupReference() { + Util.setBlock(testPositionBlockPos, block); + } + + @Override + protected void cleanupReference() { + Util.setBlock(testPositionBlockPos, Blocks.AIR); + } + + @Override + protected void setupQuartz() { + instance = Quartz.getDrawBatcherForBlock(testPosition).createInstance(testPosition, mesh, null, null, null); + if(instance == null){ + fail("Instance creation failed"); + } + } + + @Override + protected void cleanupQuartz() { + if (instance != null) { + instance.delete(); + } + } + + @OnModLoad + public static void onModLoad() { + Quartz.EVENT_BUS.addListener(BlockMimicryTest::createTestsEvent); + } + + public static void createTestsEvent(QuartzInternalEvent.CreateTests event){ + registerTest(new BlockMimicryTest(Blocks.STONE), true); + registerTest(new BlockMimicryTest(Blocks.DIORITE), true); + registerTest(new BlockMimicryTest(Blocks.POLISHED_DIORITE), true); + registerTest(new BlockMimicryTest(Blocks.ANDESITE), true); + registerTest(new BlockMimicryTest(Blocks.POLISHED_ANDESITE), true); +// registerTest(new BlockMimicryTest(Blocks.GRASS_BLOCK), true); // biome coloration issues + registerTest(new BlockMimicryTest(Blocks.DIRT), true); + registerTest(new BlockMimicryTest(Blocks.COARSE_DIRT), true); + registerTest(new BlockMimicryTest(Blocks.PODZOL), true); + registerTest(new BlockMimicryTest(Blocks.COBBLESTONE), true); + registerTest(new BlockMimicryTest(Blocks.OAK_PLANKS), true); + registerTest(new BlockMimicryTest(Blocks.SPRUCE_PLANKS), true); + registerTest(new BlockMimicryTest(Blocks.BIRCH_PLANKS), true); + registerTest(new BlockMimicryTest(Blocks.JUNGLE_PLANKS), true); + registerTest(new BlockMimicryTest(Blocks.ACACIA_PLANKS), true); + registerTest(new BlockMimicryTest(Blocks.CHERRY_PLANKS), true); + registerTest(new BlockMimicryTest(Blocks.DARK_OAK_PLANKS), true); + registerTest(new BlockMimicryTest(Blocks.MANGROVE_PLANKS), true); + registerTest(new BlockMimicryTest(Blocks.BAMBOO_PLANKS), true); + registerTest(new BlockMimicryTest(Blocks.BAMBOO_MOSAIC), true); + // ambient occlusion is force enabled by Quartz + // saplings have it force disabled because their quads aren't axis aligned +// registerTest(new BlockMimicryTest(Blocks.OAK_SAPLING), true); +// registerTest(new BlockMimicryTest(Blocks.SPRUCE_SAPLING), true); +// registerTest(new BlockMimicryTest(Blocks.BIRCH_SAPLING), true); +// registerTest(new BlockMimicryTest(Blocks.JUNGLE_SAPLING), true); +// registerTest(new BlockMimicryTest(Blocks.ACACIA_SAPLING), true); +// registerTest(new BlockMimicryTest(Blocks.CHERRY_SAPLING), true); +// registerTest(new BlockMimicryTest(Blocks.DARK_OAK_SAPLING), true); +// registerTest(new BlockMimicryTest(Blocks.MANGROVE_PROPAGULE), true); +// registerTest(new BlockMimicryTest(Blocks.BEDROCK), true); + // there are some fall blocks here, they are ignored + registerTest(new BlockMimicryTest(Blocks.GOLD_ORE), true); + registerTest(new BlockMimicryTest(Blocks.DEEPSLATE_GOLD_ORE), true); + registerTest(new BlockMimicryTest(Blocks.IRON_ORE), true); + registerTest(new BlockMimicryTest(Blocks.DEEPSLATE_IRON_ORE), true); + registerTest(new BlockMimicryTest(Blocks.COAL_ORE), true); + registerTest(new BlockMimicryTest(Blocks.DEEPSLATE_COAL_ORE), true); + registerTest(new BlockMimicryTest(Blocks.NETHER_GOLD_ORE), true); + registerTest(new BlockMimicryTest(Blocks.OAK_LOG), true); + registerTest(new BlockMimicryTest(Blocks.SPRUCE_LOG), true); + registerTest(new BlockMimicryTest(Blocks.BIRCH_LOG), true); + registerTest(new BlockMimicryTest(Blocks.JUNGLE_LOG), true); + registerTest(new BlockMimicryTest(Blocks.ACACIA_LOG), true); + registerTest(new BlockMimicryTest(Blocks.CHERRY_LOG), true); + registerTest(new BlockMimicryTest(Blocks.DARK_OAK_LOG), true); + registerTest(new BlockMimicryTest(Blocks.MANGROVE_LOG), true); + registerTest(new BlockMimicryTest(Blocks.MANGROVE_ROOTS), true); + registerTest(new BlockMimicryTest(Blocks.MUDDY_MANGROVE_ROOTS), true); + registerTest(new BlockMimicryTest(Blocks.BAMBOO_BLOCK), true); + registerTest(new BlockMimicryTest(Blocks.STRIPPED_SPRUCE_LOG), true); + registerTest(new BlockMimicryTest(Blocks.STRIPPED_BIRCH_LOG), true); + registerTest(new BlockMimicryTest(Blocks.STRIPPED_JUNGLE_LOG), true); + registerTest(new BlockMimicryTest(Blocks.STRIPPED_ACACIA_LOG), true); + registerTest(new BlockMimicryTest(Blocks.STRIPPED_CHERRY_LOG), true); + registerTest(new BlockMimicryTest(Blocks.STRIPPED_DARK_OAK_LOG), true); + registerTest(new BlockMimicryTest(Blocks.STRIPPED_OAK_LOG), true); + registerTest(new BlockMimicryTest(Blocks.STRIPPED_MANGROVE_LOG), true); + registerTest(new BlockMimicryTest(Blocks.STRIPPED_BAMBOO_BLOCK), true); + } +} diff --git a/src/test/java/net/roguelogix/quartz/testing/tests/common/ScreenshotCompareTest.java b/src/test/java/net/roguelogix/quartz/testing/tests/common/ScreenshotCompareTest.java new file mode 100644 index 0000000..e9d7904 --- /dev/null +++ b/src/test/java/net/roguelogix/quartz/testing/tests/common/ScreenshotCompareTest.java @@ -0,0 +1,170 @@ +package net.roguelogix.quartz.testing.tests.common; + +import com.mojang.blaze3d.platform.NativeImage; +import it.unimi.dsi.fastutil.objects.ReferenceArrayList; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Screenshot; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.roguelogix.phosphophyllite.debug.DebugInfo; +import net.roguelogix.phosphophyllite.util.NonnullDefault; +import net.roguelogix.quartz.DrawBatch; +import net.roguelogix.quartz.internal.QuartzCore; +import net.roguelogix.quartz.internal.QuartzInternalEvent; +import net.roguelogix.quartz.internal.gl46.GL46Core; +import net.roguelogix.quartz.testing.QuartzTest; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector3i; +import org.joml.Vector3ic; +import org.lwjgl.opengl.GL46C; +import org.lwjgl.system.MemoryUtil; + +import java.util.List; + +import static net.roguelogix.quartz.testing.tests.Util.*; + +@NonnullDefault +public abstract class ScreenshotCompareTest extends QuartzTest { + + + + + protected final Vector3ic testPosition; + protected final BlockPos testPositionBlockPos; + + private int frame; + + protected ScreenshotCompareTest(String id, Vector3ic testPosition) { + super(id); + this.testPosition = new Vector3i(testPosition); + this.testPositionBlockPos = new BlockPos(testPosition.x(), testPosition.y(), testPosition.z()); + + for (Vector3ic screenshotPosition : screenshotPositions) { + screenshotLoop.add(() -> {setEyePos(screenshotPosition);setLookCenter(testPosition.x(), testPosition.y(), testPosition.z());}); + screenshotLoop.add(this::takeScreenshot); + } + + frameActions.add(WAIT_FRAME); + frameActions.add(this::setupQuartz); + frameActions.add(() -> currentScreenshotList = quartzScreenshots); + frameActions.add(WAIT_FRAME); + frameActions.addAll(screenshotLoop); + frameActions.add(this::cleanupQuartz); + frameActions.add(this::setupReference); + frameActions.add(() -> currentScreenshotList = referenceScreenshots); + // wait a few frames for the chunk rebuild +// frameActions.add(WAIT_FRAME); + frameActions.addAll(screenshotLoop); + frameActions.add(this::cleanupReference); + frameActions.add(WAIT_FRAME); + } + + @Override + public void setup() { + + hideHUD(); + setFOV(40); + setFlying(); + + + setWindowSize(256, 256); + + teleportToInOverworld(0, 0, 0); + + frame = 0; + } + + @Override + public void cleanup() { + + quartzScreenshots.forEach(NativeImage::close); + referenceScreenshots.forEach(NativeImage::close); + quartzScreenshots.clear(); + referenceScreenshots.clear(); + } + + @Override + public boolean running() { + return frame < frameActions.size(); + } + + @Override + public void frameStart() { + + } + + private final List quartzScreenshots = new ReferenceArrayList<>(); + private final List referenceScreenshots = new ReferenceArrayList<>(); + @Nullable + private List currentScreenshotList; + + private static final Runnable WAIT_FRAME = () -> { + }; + + private static final List screenshotPositions = new ReferenceArrayList<>(); + private final List screenshotLoop = new ReferenceArrayList<>(); + private final List frameActions = new ReferenceArrayList<>(); + + static { +// screenshotPositions.add(new Vector3i(-5, -5, -5)); +// screenshotPositions.add(new Vector3i(-5, -5, +5)); +// screenshotPositions.add(new Vector3i(-5, +5, -5)); +// screenshotPositions.add(new Vector3i(-5, +5, +5)); +// screenshotPositions.add(new Vector3i(+5, -5, -5)); +// screenshotPositions.add(new Vector3i(+5, -5, +5)); +// screenshotPositions.add(new Vector3i(+5, +5, -5)); + screenshotPositions.add(new Vector3i(+5, +5, +5)); + } + + private void takeScreenshot() { + if (currentScreenshotList != null) { + currentScreenshotList.add(screenshot()); + } + } + + @Override + public void frameEnd() { + frameActions.get(frame).run(); + frame++; + if (frame == frameActions.size()) { + compareFrames(); + } + } + + public void compareFrames() { + for (int i = 0; i < referenceScreenshots.size(); i++) { + var quartzScreenshot = quartzScreenshots.get(i); + var referenceScreenshot = referenceScreenshots.get(i); + var width = referenceScreenshot.getWidth(); + var height = referenceScreenshot.getHeight(); + var totalPixels = width * height; + var quartzPixels = quartzScreenshot.pixels; + var referencePixels = referenceScreenshot.pixels; + var totalBytes = totalPixels * 4; + for (int j = 0; j < totalBytes; j++) { + final var quartzByte = Byte.toUnsignedInt(MemoryUtil.memGetByte(quartzPixels + (j))); + final var referenceByte = Byte.toUnsignedInt(MemoryUtil.memGetByte(referencePixels + (j))); + final var difference = Math.abs(quartzByte - referenceByte); + // ~1.5% error allowed, there can be tiny differences + if (difference > 4) { + var pixelIndex = j >> 2; + var w = pixelIndex % width; + var h = pixelIndex / width; + fail("Pixel (" + w + ", " + h + ") different in screenshot " + i + " difference " + difference + " in byte " + (pixelIndex & 0b11)); + return; + } + } + } + } + + @Override + public void feedbackCollected(QuartzInternalEvent.FeedbackCollected event) { + + } + + protected abstract void setupReference(); + protected abstract void cleanupReference(); + + protected abstract void setupQuartz(); + protected abstract void cleanupQuartz(); +} diff --git a/src/test/resources/assets/quartz/blockstates/quartz_test_runner_block.json b/src/test/resources/assets/quartz/blockstates/quartz_test_runner_block.json new file mode 100644 index 0000000..41ef07a --- /dev/null +++ b/src/test/resources/assets/quartz/blockstates/quartz_test_runner_block.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "phosphophyllite:block/phosphophyllite_ore" + } + } +} \ No newline at end of file diff --git a/src/test/resources/assets/quartz/models/item/quartz_test_runner_block.json b/src/test/resources/assets/quartz/models/item/quartz_test_runner_block.json new file mode 100644 index 0000000..6ed0ade --- /dev/null +++ b/src/test/resources/assets/quartz/models/item/quartz_test_runner_block.json @@ -0,0 +1,3 @@ +{ + "parent": "phosphophyllite:block/phosphophyllite_ore" +} \ No newline at end of file