Skip to content

Commit 419c8c5

Browse files
committed
Merge branch 'main' into fix/issue64-ntm-glsm
Conflict resolution highlights: - VertexAttribState: unify the parallel per-VAO FFP vertex-flag derivations (deriveVertexFlags from this branch, currentClientArrayVertexFlags from main's BPR fix) into the single currentClientArrayVertexFlags, keeping this branch's null guard; ShaderManager call sites and the branch's test updated to the unified name. - VertexAttribStateTest: use a direct buffer for the client pointer, as VertexAttribState.set now captures the native address. - docs/compatibility-matrix.md, dependencies.gradle, mixin condition/test lists: union of both sides.
2 parents 9f425eb + 6110854 commit 419c8c5

68 files changed

Lines changed: 3220 additions & 254 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.gradle

Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
12
import org.jetbrains.gradle.ext.Gradle
23

34
plugins {
@@ -95,6 +96,10 @@ java {
9596
configurations {
9697
contain
9798
implementation.extendsFrom(contain)
99+
// External classes bundled into the mod jar body under the shared shadow namespace, mirroring
100+
// the upstream Celeritas distribution layout. Kept separate from implementation so the plain
101+
// namespace jars stay on the compile/dev classpath for third-party mods that need them.
102+
shadowBundle
98103
modCompileOnly
99104
compileOnly.extendsFrom(modCompileOnly)
100105
modRuntimeOnly
@@ -281,6 +286,35 @@ def actiniumProjectAccessTransformer = propertyBool('use_access_transformer')
281286
? rootProject.projectDir.toPath().resolve("src/main/resources/${propertyString('access_transformer_locations')}").toFile()
282287
: null
283288

289+
/**
290+
* Dev-runtime image of the mod in the same shadow layout as the production jar: merged module
291+
* classes plus the shadow bundle classes relocated under
292+
* org.embeddedt.embeddium.impl.shadow.joml. The client and server runs swap the raw source-set
293+
* outputs and the plain joml jar for this artifact so mixin field descriptors seen at dev runtime
294+
* match the production distribution. The plain joml jar itself stays on the run classpath for
295+
* third-party mods that still speak org.joml.
296+
*/
297+
def devShadowJar = tasks.register('devShadowJar', ShadowJar) {
298+
group = 'build'
299+
description = 'Builds the shadow-layout dev runtime jar consumed by the client and server runs.'
300+
archiveBaseName.set('Actinium-dev')
301+
archiveClassifier.set('')
302+
archiveVersion.set('')
303+
destinationDirectory.set(layout.buildDirectory.dir('dev-libs'))
304+
configurations.set([project.configurations.shadowBundle])
305+
from(sourceSets.main.output)
306+
relocate('org.joml', 'org.embeddedt.embeddium.impl.shadow.joml')
307+
mergeServiceFiles()
308+
// Deliberately no manifest inheritance from the jar task: that manifest carries the
309+
// FMLCorePlugin / FMLCorePluginContainsFMLMod entries the production jar needs, but at dev time
310+
// the coremod is already provided through unimined's fml.coreMods.load system property. Letting
311+
// them surface on a plain classpath jar makes FML's classpath scan treat this jar as a coremod
312+
// container, which parent-delegates its package (class loader exclusion) and splits classes
313+
// like MixinLate across the app class loader and the LaunchClassLoader (ClassCastException on
314+
// the mixinbooter ILateMixinLoader cast).
315+
manifest.attributes('Implementation-Version': finalVersion)
316+
}
317+
284318
def remapCompatBridgeJar
285319

286320
unimined.minecraft {
@@ -304,12 +338,32 @@ unimined.minecraft {
304338
jvmArgs += extraArgs.split('\\s+').toList()
305339
}
306340

341+
// Swap the raw module outputs for the shadow-layout dev jar so the dev runtime carries
342+
// the same relocated joml descriptors as the production distribution. The swap happens
343+
// on the -Dcrl.dev.extrapath channel below, NOT on the JavaExec classpath: mod classes
344+
// must stay invisible to the app class loader (exactly like production, where the mod
345+
// jar is not on java.class.path). Keeping the jar on the JavaExec classpath lets the
346+
// app loader serve Actinium classes as a parent-delegation fallback and splits classes
347+
// like MixinLate across the app loader and the LaunchClassLoader (ClassCastException on
348+
// the mixinbooter ILateMixinLoader cast).
349+
def originalRunClasspath = classpath
350+
classpath = files(provider {
351+
def rawModuleOutputs = sourceSets.main.output.files
352+
originalRunClasspath.files.findAll { file ->
353+
!rawModuleOutputs.contains(file)
354+
}
355+
})
356+
307357
// Unimined omits source output paths here when a clean build has not created their directories yet.
358+
// The raw module outputs must not reach Cleanroom either: they would expose unrelocated
359+
// joml descriptors next to the shadowed ones, so the dev shadow jar replaces them here.
308360
def extraPathArgument = '-Dcrl.dev.extrapath='
361+
def rawModuleOutputPaths = project.sourceSets.main.output.files.collect { it.absolutePath } as Set
309362
def extraPaths = new LinkedHashSet<String>()
310-
extraPaths.addAll(project.sourceSets.main.output.files.collect { it.absolutePath })
363+
extraPaths.add(devShadowJar.get().archiveFile.get().asFile.absolutePath)
311364
jvmArgs.findAll { it.toString().startsWith(extraPathArgument) }.each { argument ->
312-
extraPaths.addAll(argument.toString().substring(extraPathArgument.length()).tokenize(File.pathSeparator))
365+
extraPaths.addAll(argument.toString().substring(extraPathArgument.length()).tokenize(File.pathSeparator)
366+
.findAll { path -> !rawModuleOutputPaths.contains(path) })
313367
}
314368
jvmArgs = jvmArgs.findAll { !it.toString().startsWith(extraPathArgument) }
315369
jvmArgs += "${extraPathArgument}${extraPaths.join(File.pathSeparator)}"
@@ -327,6 +381,10 @@ unimined.minecraft {
327381
enableBaseMixin()
328382
enableMixinExtra()
329383
}
384+
// Thin intermediate: the distributed jar is the shadowRemapJar output.
385+
asJar {
386+
archiveClassifier.set('thin')
387+
}
330388
}
331389

332390
remapCompatBridgeJar = remap(compatBridgeJar.get(), 'remapCompatBridgeJar') {
@@ -351,6 +409,25 @@ unimined.minecraft {
351409

352410
def compatBridgeRemapOutput = remapCompatBridgeJar.flatMap { it.archiveFile }
353411

412+
/**
413+
* Production mod jar in the upstream Celeritas distribution layout: the remapped thin jar merged
414+
* with the shadow bundle classes, with joml relocated under
415+
* org.embeddedt.embeddium.impl.shadow.joml so mixin field descriptors match the layout upstream
416+
* Celeritas addons bind against. The ContainedDeps joml jar inherited from the thin jar stays
417+
* nested for third-party mods that still consume plain org.joml.
418+
*/
419+
def shadowRemapJar = tasks.register('shadowRemapJar', ShadowJar) {
420+
group = 'build'
421+
description = 'Builds the production mod jar with the upstream shadow layout (relocated joml).'
422+
dependsOn tasks.named('remapJar')
423+
archiveClassifier.set('')
424+
configurations.set([project.configurations.shadowBundle])
425+
from(zipTree(tasks.named('remapJar').get().archiveFile))
426+
relocate('org.joml', 'org.embeddedt.embeddium.impl.shadow.joml')
427+
mergeServiceFiles()
428+
manifest.inheritFrom(tasks.named('jar').get().manifest)
429+
}
430+
354431
/**
355432
* Chunk Animator's dev-environment coremod (MCPNames) reads ./../mcp/methods.csv and
356433
* ./../mcp/fields.csv relative to the client run directory and crashes at class init when they
@@ -414,15 +491,18 @@ def prepareCompatBridgeRun = tasks.register('prepareCompatBridgeRun') {
414491

415492
tasks.named('preRunClient').configure {
416493
dependsOn tasks.named('classes')
494+
dependsOn devShadowJar
417495
dependsOn prepareCompatBridgeRun
418496
dependsOn prepareChunkAnimatorMcpMappings
419497
}
420498

421499
tasks.named('preRunServer').configure {
422500
dependsOn tasks.named('classes')
501+
dependsOn devShadowJar
423502
}
424503

425504
tasks.named('assemble').configure {
505+
dependsOn shadowRemapJar
426506
dependsOn remapCompatBridgeJar
427507
dependsOn compatBridgeSourcesJar
428508
}
@@ -619,13 +699,13 @@ tasks.named('processResources').configure {
619699
from('THIRD_PARTY_NOTICES.md', 'LICENSE-REESES-SODIUM-OPTIONS.md')
620700
}
621701

622-
def verifyRemapJar = tasks.register('verifyRemapJar') {
623-
dependsOn tasks.named('remapJar')
624-
def remapOutput = tasks.named('remapJar').flatMap { it.archiveFile }
625-
inputs.file(remapOutput)
702+
def verifyDistributedJar = tasks.register('verifyDistributedJar') {
703+
dependsOn tasks.named('shadowRemapJar')
704+
def shadowOutput = tasks.named('shadowRemapJar').flatMap { it.archiveFile }
705+
inputs.file(shadowOutput)
626706

627707
doLast {
628-
def archive = zipTree(remapOutput.get().asFile)
708+
def archive = zipTree(shadowOutput.get().asFile)
629709
def requiredEntries = [
630710
'META-INF/MANIFEST.MF',
631711
'THIRD_PARTY_NOTICES.md',
@@ -634,31 +714,39 @@ def verifyRemapJar = tasks.register('verifyRemapJar') {
634714
'com/gtnewhorizons/angelica/glsm/GLStateManager.class',
635715
'net/coderbot/iris/Iris.class',
636716
'org/embeddedt/embeddium/impl/render/chunk/RenderSectionManager.class',
717+
'org/embeddedt/embeddium/impl/shadow/joml/Vector3f.class',
718+
'joml-1.10.5.jar',
637719
'mixins.actinium.iris.json',
638720
'mixins.actinium.vintage.json',
639721
'META-INF/services/com.gtnewhorizons.angelica.glsm.backend.RenderBackend'
640722
]
641723

642724
requiredEntries.each { entry ->
643725
if (archive.matching { include entry }.isEmpty()) {
644-
throw new GradleException("Remapped mod jar is missing required entry: ${entry}")
726+
throw new GradleException("Distributed mod jar is missing required entry: ${entry}")
645727
}
646728
}
647729

648-
def manifest = new java.util.jar.JarFile(remapOutput.get().asFile).withCloseable {
730+
// The shadow layout only holds relocated joml classes in the jar body; plain org.joml is
731+
// provided to third-party mods by the nested ContainedDeps jar instead.
732+
if (!archive.matching { include 'org/joml/**/*.class' }.isEmpty()) {
733+
throw new GradleException('Distributed mod jar leaks plain org.joml classes into the jar body')
734+
}
735+
736+
def manifest = new java.util.jar.JarFile(shadowOutput.get().asFile).withCloseable {
649737
it.manifest.mainAttributes
650738
}
651739
if (manifest.getValue('FMLCorePlugin') != propertyString('coremod_plugin_class_name')) {
652-
throw new GradleException('Remapped mod jar has an invalid FMLCorePlugin manifest entry')
740+
throw new GradleException('Distributed mod jar has an invalid FMLCorePlugin manifest entry')
653741
}
654742
if (manifest.getValue('FMLAT') != propertyString('access_transformer_locations').tokenize('/\\').last()) {
655-
throw new GradleException('Remapped mod jar has an invalid FMLAT manifest entry')
743+
throw new GradleException('Distributed mod jar has an invalid FMLAT manifest entry')
656744
}
657745

658-
new java.util.jar.JarFile(remapOutput.get().asFile).withCloseable { jar ->
746+
new java.util.jar.JarFile(shadowOutput.get().asFile).withCloseable { jar ->
659747
def metadataEntry = jar.getEntry('mcmod.info')
660748
if (metadataEntry == null) {
661-
throw new GradleException('Remapped mod jar is missing mcmod.info')
749+
throw new GradleException('Distributed mod jar is missing mcmod.info')
662750
}
663751
def metadata = new groovy.json.JsonSlurper().parse(jar.getInputStream(metadataEntry))
664752
def mod = metadata[0]
@@ -669,10 +757,10 @@ def verifyRemapJar = tasks.register('verifyRemapJar') {
669757
|| mod.url != propertyString('mod_url')
670758
|| mod.updateJSON != propertyString('mod_update_json')
671759
|| mod.logoFile != propertyString('mod_logo_path')) {
672-
throw new GradleException('Remapped mod jar metadata does not match Gradle mod metadata properties')
760+
throw new GradleException('Distributed mod jar metadata does not match Gradle mod metadata properties')
673761
}
674762
if (!mod.logoFile.isEmpty() && jar.getEntry(mod.logoFile) == null) {
675-
throw new GradleException("Remapped mod jar is missing configured logo: ${mod.logoFile}")
763+
throw new GradleException("Distributed mod jar is missing configured logo: ${mod.logoFile}")
676764
}
677765
}
678766
}
@@ -740,7 +828,7 @@ def verifyModuleBoundaries = tasks.register('verifyModuleBoundaries') {
740828
}
741829

742830
tasks.named('check').configure {
743-
dependsOn verifyRemapJar
831+
dependsOn verifyDistributedJar
744832
dependsOn verifyCompatBridgeJar
745833
dependsOn verifyModuleBoundaries
746834
}

celeritas-common/src/main/java/org/embeddedt/embeddium/impl/biome/BiomeColorCache.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,39 @@ private Slice[] initializeSlices() {
8383

8484
protected abstract int resolveColor(RESOLVER resolver, BIOME biome, int relativeX, int relativeY, int relativeZ);
8585

86+
/**
87+
* Controls whether the color post-processing hook {@link #postProcessColor} should run over each populated
88+
* slice. Returns {@code false} by default, so caches which do not need positional color adjustments skip
89+
* the extra pass entirely.
90+
*
91+
* @return {@code true} to post-process the cached colors of every populated slice, {@code false} otherwise
92+
*/
93+
protected boolean shouldPostProcessColors() {
94+
return false;
95+
}
96+
97+
/**
98+
* Applies a positional adjustment to a biome color after biome blending (including box blur) has finished.
99+
* This hook allows the host mod to adjust cached colors based on their world position, e.g. to implement
100+
* biome color noise.
101+
*
102+
* <p>The returned color is cached in the color buffer and reused across mesh rebuilds, so this function
103+
* must be a pure function of its arguments and must not depend on mutable external state.</p>
104+
*
105+
* <p>The hook runs for both the blurred path and the fast path where an entire slice resolved to a single
106+
* uniform color (which skips blurring). This is the reason it is applied after the blur step.</p>
107+
*
108+
* @param resolver the color resolver the slice is currently populated for
109+
* @param worldX the world-space X coordinate of the color being adjusted
110+
* @param worldY the world-space Y coordinate of the color being adjusted
111+
* @param worldZ the world-space Z coordinate of the color being adjusted
112+
* @param color the blended (or directly resolved, if no blur was applied) ARGB color
113+
* @return the adjusted color to store in the cache
114+
*/
115+
protected int postProcessColor(RESOLVER resolver, int worldX, int worldY, int worldZ, int color) {
116+
return color;
117+
}
118+
86119
private void updateColorBuffers(int relY, RESOLVER resolver, Slice slice) {
87120
int worldY = this.minY + relY;
88121

@@ -117,6 +150,16 @@ private void updateColorBuffers(int relY, RESOLVER resolver, Slice slice) {
117150
BoxBlur.blur(slice.buffer, this.tempColorBuffer, this.blendRadius);
118151
}
119152

153+
if (this.shouldPostProcessColors()) {
154+
for (int worldZ = this.minZ; worldZ <= this.maxZ; worldZ++) {
155+
for (int worldX = this.minX; worldX <= this.maxX; worldX++) {
156+
int relativeX = worldX - this.minX;
157+
int relativeZ = worldZ - this.minZ;
158+
slice.buffer.set(relativeX, relativeZ, this.postProcessColor(resolver, worldX, worldY, worldZ, slice.buffer.get(relativeX, relativeZ)));
159+
}
160+
}
161+
}
162+
120163
slice.lastPopulateStamp = this.populateStamp;
121164
}
122165

celeritas-common/src/main/java/org/embeddedt/embeddium/impl/gui/SodiumGameOptions.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ public static class QualitySettings {
113113
public int chunkFadeInDuration = 0;
114114

115115
public int legacyBiomeBlendRadius = 0;
116+
117+
// Biome color position noise (issue #56, inspired by Ambient Environment). Applied after
118+
// biome blending so the per-position variation survives blend averaging, with a
119+
// multiplicative factor of 1 +/- intensity so the result stays centered on the mean.
120+
public boolean useBiomeColorNoise = true;
121+
public float biomeColorNoiseGrassIntensity = 0.08F;
122+
public float biomeColorNoiseFoliageIntensity = 0.08F;
123+
public float biomeColorNoiseWaterIntensity = 0.08F;
116124
}
117125

118126
public static class NotificationSettings {

celeritas-common/src/main/java/org/embeddedt/embeddium/impl/render/chunk/RenderSectionManager.java

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,6 @@ private void checkTranslucencyChange() {
283283
private void scheduleTranslucencyUpdates(int camSectionX, int camSectionY, int camSectionZ) {
284284
var renderListManager = this.getCurrentRenderListManager();
285285
var rebuildLists = renderListManager.getRebuildLists().byUpdateType();
286-
var sortRebuildList = rebuildLists.get(ChunkUpdateType.SORT);
287-
var importantSortRebuildList = rebuildLists.get(ChunkUpdateType.IMPORTANT_SORT);
288286
var allowImportant = allowImportantRebuilds();
289287
var translucentPass = this.renderPassConfiguration.defaultTranslucentMaterial().pass;
290288
if (!this.hasTranslucencySortedSections()) {
@@ -331,8 +329,8 @@ private void scheduleTranslucencyUpdates(int camSectionX, int camSectionY, int c
331329

332330
if (cameraChangedSection || section.isAlignedWithSectionOnGrid(camSectionX, camSectionY, camSectionZ)) {
333331
section.setPendingUpdate(update);
334-
// Inject it into the rebuild lists
335-
(update == ChunkUpdateType.IMPORTANT_SORT ? importantSortRebuildList : sortRebuildList).add(section);
332+
// Inject it into the appropriate list
333+
rebuildLists.get(update).add(section);
336334

337335
section.lastCameraX = cameraPosition.x;
338336
section.lastCameraY = cameraPosition.y;
@@ -492,10 +490,25 @@ private boolean rebuildListHasUpdates() {
492490
* Inject sections that requested a rebuild between graph updates into the appropriate rebuild lists.
493491
*/
494492
private void promoteInterimRebuildList() {
493+
if (this.sectionsRequestingUpdate.isEmpty()) {
494+
return;
495+
}
496+
495497
var rebuildLists = this.getCurrentRenderListManager().getRebuildLists().byUpdateType();
498+
boolean graphUpdatePending = this.getCurrentRenderListManager().isNeedsUpdate();
499+
496500
for (var section : this.sectionsRequestingUpdate) {
497-
rebuildLists.get(section.getPendingUpdate()).add(section);
501+
var updateType = section.getPendingUpdate();
502+
if (updateType == null) {
503+
// should never happen, but be defensive
504+
continue;
505+
}
506+
if (!graphUpdatePending || updateType.isImportant()) {
507+
rebuildLists.get(updateType).add(section);
508+
}
498509
}
510+
511+
this.sectionsRequestingUpdate.clear();
499512
}
500513

501514
public void updateChunks(boolean updateImmediately) {
@@ -511,13 +524,7 @@ public void updateChunks(boolean updateImmediately) {
511524
this.builder.tickSchedulingBudget();
512525
}
513526

514-
// Promotion of the interim rebuild list is not required if a graph update is requested, as the graph
515-
// generates a new rebuild list anyway
516-
if (!this.renderListManager.isNeedsUpdate() && !sectionsRequestingUpdate.isEmpty()) {
517-
this.promoteInterimRebuildList();
518-
}
519-
520-
this.sectionsRequestingUpdate.clear();
527+
this.promoteInterimRebuildList();
521528

522529
if (!rebuildListHasUpdates()) {
523530
// Nothing was dispatched, so the workers cannot have been starved for lack of budget.
@@ -887,7 +894,12 @@ protected void scheduleSectionForRebuild(int x, int y, int z, boolean important)
887894
}
888895

889896
if (section.requestUpdate(pendingUpdate) || cancelledInFlightBuild) {
890-
if (!this.getCurrentRenderListManager().isNeedsUpdate() && this.sectionsRequestingUpdate.size() < this.builder.getSchedulingBudget()) {
897+
// Check importance using the section's new update type, as it may not be exactly what we requested
898+
important = section.getPendingUpdate().isImportant();
899+
900+
if (important ||
901+
(!this.getCurrentRenderListManager().isNeedsUpdate() &&
902+
this.sectionsRequestingUpdate.size() < this.builder.getSchedulingBudget())) {
891903
this.sectionsRequestingUpdate.add(section);
892904
} else {
893905
this.markGraphDirty();

0 commit comments

Comments
 (0)