diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/MainWorldService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/MainWorldService.java
new file mode 100644
index 000000000..2906f6750
--- /dev/null
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/MainWorldService.java
@@ -0,0 +1,199 @@
+/*
+ * Iris is a World Generator for Minecraft Servers
+ * Copyright (c) 2026 Arcane Arts (Volmit Software)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package art.arcane.iris.modded;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Stream;
+
+public final class MainWorldService {
+ private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
+ private static final String PRESET_NAMESPACE = "irisworldgen";
+ private static final String MARKER_NAME = "mainworld.pending";
+ private static final String[] VANILLA_DIMENSION_FOLDERS = {
+ "region",
+ "entities",
+ "poi",
+ "mantle",
+ "dimensions/minecraft/overworld",
+ "dimensions/minecraft/the_nether",
+ "dimensions/minecraft/the_end",
+ "DIM-1",
+ "DIM1"
+ };
+
+ private MainWorldService() {
+ }
+
+ public static String presetIdFor(String packRef) {
+ String value = packRef.trim();
+ int colon = value.indexOf(':');
+ String pack = colon >= 0 ? value.substring(0, colon) : value;
+ String dimension = colon >= 0 ? value.substring(colon + 1) : value;
+ String presetKey = dimension.equals(pack) ? pack : pack + "_" + dimension;
+ return PRESET_NAMESPACE + ":" + presetKey;
+ }
+
+ public static void reconcileEarly() {
+ try {
+ ModdedModConfig config = ModdedModConfig.get();
+ String pack = config.mainWorldPack();
+ if (pack == null || pack.isBlank()) {
+ return;
+ }
+ Path properties = instanceRoot().resolve("server.properties");
+ String target = presetIdFor(pack);
+ String currentType = readProperty(properties, "level-type");
+ if (!target.equals(currentType)) {
+ writeLevelProperties(properties, target, config.mainWorldSeed());
+ markPending();
+ LOGGER.warn("Iris main world '{}' staged: server.properties level-type set to {}. Restart again to generate it (this boot still uses the previous overworld; player data is kept).", pack, target);
+ return;
+ }
+ if (!isPending()) {
+ return;
+ }
+ String levelName = firstNonBlank(readProperty(properties, "level-name"), "world");
+ wipeVanillaDimensions(instanceRoot().resolve(levelName));
+ clearPending();
+ LOGGER.warn("Iris main world '{}' generated fresh: cleared the previous overworld/nether/end so this boot regenerates them as {} (player data kept).", pack, target);
+ } catch (Throwable e) {
+ LOGGER.error("Iris main world reconciliation failed", e);
+ }
+ }
+
+ public static boolean stage(String packRef, long seed) {
+ try {
+ Path properties = instanceRoot().resolve("server.properties");
+ writeLevelProperties(properties, presetIdFor(packRef), seed);
+ markPending();
+ return true;
+ } catch (IOException e) {
+ LOGGER.error("Iris failed to stage the main world in server.properties", e);
+ return false;
+ }
+ }
+
+ public static void clearOverride() {
+ try {
+ clearPending();
+ } catch (IOException e) {
+ LOGGER.error("Iris failed to clear the pending main world marker", e);
+ }
+ }
+
+ private static Path instanceRoot() {
+ return ModdedEngineBootstrap.loader().configDir().getParent();
+ }
+
+ private static Path markerFile() {
+ return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve(MARKER_NAME);
+ }
+
+ private static boolean isPending() {
+ return Files.isRegularFile(markerFile());
+ }
+
+ private static void markPending() throws IOException {
+ Path marker = markerFile();
+ Files.createDirectories(marker.getParent());
+ Files.writeString(marker, "pending", StandardCharsets.UTF_8);
+ }
+
+ private static void clearPending() throws IOException {
+ Files.deleteIfExists(markerFile());
+ }
+
+ private static String readProperty(Path properties, String key) throws IOException {
+ if (!Files.isRegularFile(properties)) {
+ return null;
+ }
+ List lines = Files.readAllLines(properties, StandardCharsets.UTF_8);
+ String prefix = key + "=";
+ for (String line : lines) {
+ if (line.startsWith(prefix)) {
+ return unescape(line.substring(prefix.length()).trim());
+ }
+ }
+ return null;
+ }
+
+ private static void writeLevelProperties(Path properties, String target, long seed) throws IOException {
+ List lines = Files.isRegularFile(properties)
+ ? new ArrayList<>(Files.readAllLines(properties, StandardCharsets.UTF_8))
+ : new ArrayList<>();
+ setProperty(lines, "level-type", escape(target));
+ if (seed != 0L) {
+ setProperty(lines, "level-seed", Long.toString(seed));
+ }
+ Files.write(properties, lines, StandardCharsets.UTF_8);
+ }
+
+ private static void setProperty(List lines, String key, String value) {
+ String prefix = key + "=";
+ for (int i = 0; i < lines.size(); i++) {
+ if (lines.get(i).startsWith(prefix)) {
+ lines.set(i, prefix + value);
+ return;
+ }
+ }
+ lines.add(prefix + value);
+ }
+
+ private static void wipeVanillaDimensions(Path worldRoot) throws IOException {
+ Files.deleteIfExists(worldRoot.resolve("level.dat"));
+ Files.deleteIfExists(worldRoot.resolve("level.dat_old"));
+ for (String folder : VANILLA_DIMENSION_FOLDERS) {
+ deleteRecursively(worldRoot.resolve(folder));
+ }
+ }
+
+ private static void deleteRecursively(Path path) throws IOException {
+ if (!Files.exists(path)) {
+ return;
+ }
+ List entries = new ArrayList<>();
+ try (Stream walk = Files.walk(path)) {
+ walk.sorted(Comparator.comparingInt(Path::getNameCount).reversed()).forEach(entries::add);
+ }
+ for (Path entry : entries) {
+ Files.deleteIfExists(entry);
+ }
+ }
+
+ private static String escape(String value) {
+ return value.replace(":", "\\:");
+ }
+
+ private static String unescape(String value) {
+ return value.replace("\\:", ":").replace("\\=", "=");
+ }
+
+ private static String firstNonBlank(String value, String fallback) {
+ return value == null || value.isBlank() ? fallback : value;
+ }
+}
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java
index 036b4d423..0cd06d720 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java
@@ -116,6 +116,7 @@ public final class ModdedEngineBootstrap {
ModdedIrisLog.info("Iris " + moddedLoader.modVersion() + " bootstrapping on Minecraft " + moddedLoader.minecraftVersion() + " (" + loaderDescription + ")");
selfTest(moddedLoader.getClass().getClassLoader());
bind();
+ MainWorldService.reconcileEarly();
chunkGeneratorRegistration.run();
ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris");
armParityProbe();
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedModConfig.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedModConfig.java
index 5d02e65f5..025e38cb6 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedModConfig.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedModConfig.java
@@ -36,12 +36,19 @@ public final class ModdedModConfig {
private final boolean autoDownloadDefaultPack;
private final String primaryWorld;
private final boolean routePlayersToPrimaryWorld;
+ private final String mainWorldPack;
+ private final long mainWorldSeed;
+ private final boolean mainWorldAutoRestart;
- private ModdedModConfig(String defaultPack, boolean autoDownloadDefaultPack, String primaryWorld, boolean routePlayersToPrimaryWorld) {
+ private ModdedModConfig(String defaultPack, boolean autoDownloadDefaultPack, String primaryWorld, boolean routePlayersToPrimaryWorld,
+ String mainWorldPack, long mainWorldSeed, boolean mainWorldAutoRestart) {
this.defaultPack = defaultPack;
this.autoDownloadDefaultPack = autoDownloadDefaultPack;
this.primaryWorld = primaryWorld == null ? "" : primaryWorld.trim();
this.routePlayersToPrimaryWorld = routePlayersToPrimaryWorld;
+ this.mainWorldPack = mainWorldPack == null ? "" : mainWorldPack.trim();
+ this.mainWorldSeed = mainWorldSeed;
+ this.mainWorldAutoRestart = mainWorldAutoRestart;
}
public static ModdedModConfig get() {
@@ -61,7 +68,18 @@ public final class ModdedModConfig {
public static void setPrimaryWorld(String dimensionId) {
synchronized (LOCK) {
ModdedModConfig current = get();
- ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.autoDownloadDefaultPack, dimensionId, current.routePlayersToPrimaryWorld);
+ ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.autoDownloadDefaultPack, dimensionId, current.routePlayersToPrimaryWorld,
+ current.mainWorldPack, current.mainWorldSeed, current.mainWorldAutoRestart);
+ instance = updated;
+ write(configFile(), updated);
+ }
+ }
+
+ public static void setMainWorld(String packRef, long seed) {
+ synchronized (LOCK) {
+ ModdedModConfig current = get();
+ ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.autoDownloadDefaultPack, current.primaryWorld, current.routePlayersToPrimaryWorld,
+ packRef == null ? "" : packRef.trim(), seed, current.mainWorldAutoRestart);
instance = updated;
write(configFile(), updated);
}
@@ -83,13 +101,25 @@ public final class ModdedModConfig {
return routePlayersToPrimaryWorld;
}
+ public String mainWorldPack() {
+ return mainWorldPack;
+ }
+
+ public long mainWorldSeed() {
+ return mainWorldSeed;
+ }
+
+ public boolean mainWorldAutoRestart() {
+ return mainWorldAutoRestart;
+ }
+
private static Path configFile() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("modded.json");
}
private static ModdedModConfig load() {
Path file = configFile();
- ModdedModConfig defaults = new ModdedModConfig("overworld", true, "", true);
+ ModdedModConfig defaults = new ModdedModConfig("overworld", true, "", true, "", 0L, false);
if (!Files.isRegularFile(file)) {
write(file, defaults);
return defaults;
@@ -100,7 +130,10 @@ public final class ModdedModConfig {
json.optString("defaultPack", defaults.defaultPack),
json.optBoolean("autoDownloadDefaultPack", defaults.autoDownloadDefaultPack),
json.optString("primaryWorld", defaults.primaryWorld),
- json.optBoolean("routePlayersToPrimaryWorld", defaults.routePlayersToPrimaryWorld));
+ json.optBoolean("routePlayersToPrimaryWorld", defaults.routePlayersToPrimaryWorld),
+ json.optString("mainWorldPack", defaults.mainWorldPack),
+ json.optLong("mainWorldSeed", defaults.mainWorldSeed),
+ json.optBoolean("mainWorldAutoRestart", defaults.mainWorldAutoRestart));
} catch (RuntimeException | IOException e) {
LOGGER.error("Iris modded config at {} is invalid; using defaults", file, e);
return defaults;
@@ -113,6 +146,9 @@ public final class ModdedModConfig {
json.put("autoDownloadDefaultPack", config.autoDownloadDefaultPack);
json.put("primaryWorld", config.primaryWorld);
json.put("routePlayersToPrimaryWorld", config.routePlayersToPrimaryWorld);
+ json.put("mainWorldPack", config.mainWorldPack);
+ json.put("mainWorldSeed", config.mainWorldSeed);
+ json.put("mainWorldAutoRestart", config.mainWorldAutoRestart);
try {
Files.createDirectories(file.getParent());
Files.writeString(file, json.toString(4), StandardCharsets.UTF_8);
diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java
index 21b8ecf1b..aba61006c 100644
--- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java
+++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java
@@ -23,6 +23,7 @@ import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
+import art.arcane.iris.modded.MainWorldService;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedModConfig;
@@ -75,6 +76,7 @@ public final class ModdedWorldCommands {
root.then(enableTree("enable"));
root.then(enableTree("create"));
root.then(replaceOverworldTree());
+ root.then(mainWorldTree());
root.then(disableTree());
root.then(deleteTree("delete"));
@@ -123,6 +125,20 @@ public final class ModdedWorldCommands {
StringArgumentType.getString(context, "seed")))));
}
+ private static LiteralArgumentBuilder mainWorldTree() {
+ return Commands.literal("mainworld")
+ .then(Commands.literal("off")
+ .executes((CommandContext context) -> clearMainWorld(context.getSource())))
+ .then(Commands.argument("pack", StringArgumentType.string()).suggests(IrisModdedCommands.PACK_NAMES)
+ .executes((CommandContext context) -> mainWorld(context.getSource(),
+ StringArgumentType.getString(context, "pack"),
+ null))
+ .then(Commands.argument("seed", StringArgumentType.word())
+ .executes((CommandContext context) -> mainWorld(context.getSource(),
+ StringArgumentType.getString(context, "pack"),
+ StringArgumentType.getString(context, "seed")))));
+ }
+
public static int createWorld(CommandSourceStack source, String name, String pack, long seed) {
String[] packRef = parsePackRef(pack);
return enable(source, name, packRef[0], packRef[1], seed);
@@ -223,6 +239,87 @@ public final class ModdedWorldCommands {
return 1;
}
+ private static int clearMainWorld(CommandSourceStack source) {
+ ModdedModConfig.setMainWorld("", 0L);
+ MainWorldService.clearOverride();
+ IrisModdedCommands.ok(source, "Iris main world override cleared. The overworld keeps its current generator; edit server.properties level-type and restart to change it back.");
+ return 1;
+ }
+
+ private static int mainWorld(CommandSourceStack source, String packRaw, String seedRaw) {
+ MinecraftServer server = source.getServer();
+ long seed;
+ if (seedRaw == null || seedRaw.isBlank()) {
+ seed = 0L;
+ } else if (seedRaw.equalsIgnoreCase("random")) {
+ long rolled = ThreadLocalRandom.current().nextLong();
+ seed = rolled == 0L ? 1L : rolled;
+ } else {
+ try {
+ seed = Long.parseLong(seedRaw.trim());
+ } catch (NumberFormatException e) {
+ IrisModdedCommands.fail(source, "Invalid seed '" + seedRaw + "'. Use a number or 'random'.");
+ return 0;
+ }
+ }
+ String[] packRef = parsePackRef(packRaw);
+ String pack = packRef[0];
+ String packDimension = packRef[1];
+ if (!validPackRef(source, pack, packDimension)) {
+ return 0;
+ }
+ File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
+ if (packFolder.isDirectory()) {
+ return applyMainWorld(source, pack, packDimension, packRaw, seed);
+ }
+ IrisModdedCommands.ok(source, "Pack '" + pack + "' is not installed; downloading IrisDimensions/" + pack + "...");
+ Thread thread = new Thread(() -> {
+ boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, "master",
+ (String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
+ server.execute(() -> {
+ if (!installed || !packFolder.isDirectory()) {
+ IrisModdedCommands.fail(source, "Pack '" + pack + "' could not be downloaded; check the name or install it with /iris download " + pack + ".");
+ return;
+ }
+ applyMainWorld(source, pack, packDimension, packRaw, seed);
+ });
+ }, "Iris Main World Pack Download");
+ thread.setDaemon(true);
+ thread.start();
+ return 1;
+ }
+
+ private static int applyMainWorld(CommandSourceStack source, String pack, String packDimension, String packRef, long seed) {
+ try {
+ if (!loadPackDimension(source, pack, packDimension)) {
+ return 0;
+ }
+ } catch (Throwable e) {
+ LOGGER.error("Iris main world pack load failed for {} (dim={})", pack, packDimension, e);
+ IrisModdedCommands.fail(source, "Pack '" + pack + "' is not ready yet (still loading or validating). Try the command again in a moment.");
+ return 0;
+ }
+ if (blockIfPackBroken(source, "the main world", pack)) {
+ return 0;
+ }
+ ModdedModConfig.setMainWorld(packRef, seed);
+ if (!MainWorldService.stage(packRef, seed)) {
+ IrisModdedCommands.fail(source, "Failed to write server.properties; check file permissions and set level-type manually.");
+ return 0;
+ }
+ String preset = MainWorldService.presetIdFor(packRef);
+ IrisModdedCommands.ok(source, "Iris main world set to '" + pack + "' (preset " + preset + ", seed " + (seed == 0L ? "random" : Long.toString(seed)) + ").");
+ IrisModdedCommands.ok(source, "server.properties level-type is now " + preset + ". On the next restart the overworld, nether, and end regenerate as this Iris world.");
+ IrisModdedCommands.ok(source, "Player data (inventories, advancements, stats) is kept; existing terrain in those dimensions is replaced. This applies once.");
+ if (ModdedModConfig.get().mainWorldAutoRestart()) {
+ IrisModdedCommands.ok(source, "mainWorldAutoRestart is enabled - stopping the server now so your restart wrapper brings it back on the new main world.");
+ source.getServer().halt(false);
+ } else {
+ IrisModdedCommands.ok(source, "Restart the server now to generate it. (Set mainWorldAutoRestart=true in modded.json to have Iris stop the server for you.)");
+ }
+ return 1;
+ }
+
private static boolean blockIfPackBroken(CommandSourceStack source, String dimensionId, String pack) {
PackValidationResult validation = PackValidationRegistry.get(pack);
if (validation == null || validation.isLoadable()) {
diff --git a/docs/superpowers/2026-06-27-x86-simd-validation-handoff.md b/docs/superpowers/2026-06-27-x86-simd-validation-handoff.md
deleted file mode 100644
index 811635eda..000000000
--- a/docs/superpowers/2026-06-27-x86-simd-validation-handoff.md
+++ /dev/null
@@ -1,145 +0,0 @@
-# x86 SIMD Validation Handoff — Iris 2D Simplex Noise Kernel
-
-**Paste this whole file to the Claude Code session on the x86 PC.** It is a self-contained task. The Mac that built this is Apple Silicon (NEON, 2 lanes, no hardware gather), where the new vectorized noise kernel is a measured *loss* and is deliberately gated OFF. This machine (x86 with AVX2 = 4 lanes or AVX-512 = 8 lanes, with hardware gather) is where the kernel is expected to *win*. Your job is to verify (a) it is still **bit-exact** at 4/8 lanes and (b) **how much faster** it actually is.
-
----
-
-## Background (what exists)
-
-A determinism-safe, coordinate-parallel (structure-of-arrays) `jdk.incubator.vector` kernel for 2D FBM simplex noise was added to Iris, with a scalar fallback:
-
-- `core/src/main/java/art/arcane/iris/util/simd/NoiseKernels2D.java` — interface
-- `core/src/main/java/art/arcane/iris/util/simd/ScalarNoiseKernels2D.java` — scalar reference (mirrors `FastNoiseDouble`)
-- `core/src/main/java/art/arcane/iris/util/simd/VectorNoiseKernels2D.java` — Vector-API impl; `profitable() = lanesAligned() && DoubleVector.SPECIES_PREFERRED.length() >= 4`
-- `core/src/main/java/art/arcane/iris/util/simd/SimdSupport.java` — `noiseKernels2D()` selects vector only when `profitable()`, else scalar
-- `core/src/test/java/art/arcane/iris/util/simd/NoiseKernels2DParityTest.java` — 6 bit-exact (`0D`) parity tests vs `FastNoiseDouble`
-
-Every lane computes one coordinate's identical scalar op sequence, so results are bit-identical to scalar by construction — **but this has only ever executed at 2 lanes.** You are the first to run it at 4/8.
-
-Java 25 is required. The Gradle test JVM already adds `--add-modules jdk.incubator.vector` (in `core/build.gradle`).
-
----
-
-## Your tasks (run from the Iris project root)
-
-### 1. Confirm the host actually vectorizes ≥4 lanes
-
-Run this throwaway check (or infer from step 2's skip count):
-
-```bash
-cat > /tmp/Lanes.java <<'EOF'
-import jdk.incubator.vector.DoubleVector;
-public class Lanes {
- public static void main(String[] a){
- System.out.println("arch="+System.getProperty("os.arch")
- +" doubleLanes="+DoubleVector.SPECIES_PREFERRED.length()
- +" shape="+DoubleVector.SPECIES_PREFERRED.vectorShape());
- }
-}
-EOF
-java --add-modules jdk.incubator.vector /tmp/Lanes.java
-```
-
-Expect `doubleLanes=4` (AVX2) or `8` (AVX-512). If it prints `2`, this host is NOT a wider-vector machine and the rest of the validation won't be meaningful — report that and stop.
-
-### 2. Bit-exactness at 4/8 lanes (the determinism gate)
-
-```bash
-./gradlew :core:test --tests 'art.arcane.iris.util.simd.NoiseKernels2DParityTest' --rerun-tasks
-```
-
-PASS criteria — read `core/build/test-results/test/TEST-art.arcane.iris.util.simd.NoiseKernels2DParityTest.xml`:
-- `tests="6" failures="0" errors="0" skipped="0"`
-- **`skipped="0"` is critical**: the vector parity tests are guarded by `assumeTrue(VectorNoiseKernels2D.lanesAligned())`. On this host they MUST run (not skip), exercising the vector path at this host's lane width. A skip means the vector path didn't execute — investigate.
-- Any single non-zero delta is a determinism failure: the JDK's AVX2/AVX-512 `D2L`/`L2D`/mask-cast/gather intrinsics would have diverged from scalar. That is a hard blocker — capture the exact failing test, octave, index, expected vs actual.
-
-### 3. Measure the real speedup
-
-The benchmark harness was removed from the Mac (it was a measurement, not shipped). Re-create it here, run it, record the number, then delete it.
-
-Create `core/src/test/java/art/arcane/iris/util/simd/NoiseKernels2DBenchmarkHarness.java`:
-
-```java
-package art.arcane.iris.util.simd;
-
-import art.arcane.volmlib.util.math.RNG;
-import org.junit.Test;
-
-public class NoiseKernels2DBenchmarkHarness {
- @Test
- public void benchmark() {
- if (!Boolean.getBoolean("noise.bench")) {
- return;
- }
- int length = 256;
- int octaves = 4;
- double[] xs = new double[length];
- double[] zs = new double[length];
- double[] out = new double[length];
- RNG rng = new RNG(5L);
- for (int k = 0; k < length; k++) {
- xs[k] = (rng.nextDouble() - 0.5D) * 1_000_000D;
- zs[k] = (rng.nextDouble() - 0.5D) * 1_000_000D;
- }
- NoiseKernels2D scalar = new ScalarNoiseKernels2D();
- NoiseKernels2D vector = new VectorNoiseKernels2D();
- long scalarNs = time(scalar, octaves, xs, zs, out, length);
- long vectorNs = time(vector, octaves, xs, zs, out, length);
- System.out.println("BENCH lanes=" + VectorNoiseKernels2D.profitable()
- + " doubleLanes=" + jdk.incubator.vector.DoubleVector.SPECIES_PREFERRED.length()
- + " scalarNsPerCol=" + scalarNs + " vectorNsPerCol=" + vectorNs
- + " speedup=" + String.format("%.2f", (double) scalarNs / (double) vectorNs));
- }
-
- private static long time(NoiseKernels2D kernel, int octaves, double[] xs, double[] zs, double[] out, int length) {
- for (int w = 0; w < 20_000; w++) {
- kernel.simplexFractalFBM(123L, octaves, 0.01D, 2.0D, 0.5D, 0.6667D, xs, zs, out, length);
- }
- long start = System.nanoTime();
- int iters = 200_000;
- for (int it = 0; it < iters; it++) {
- kernel.simplexFractalFBM(123L, octaves, 0.01D, 2.0D, 0.5D, 0.6667D, xs, zs, out, length);
- }
- long elapsed = System.nanoTime() - start;
- return elapsed / ((long) iters * (long) length);
- }
-}
-```
-
-Add this temporary block INSIDE the existing `tasks.named('test', Test) { ... }` in `core/build.gradle` (use the Edit tool; do not disturb the existing `jvmArgs('--add-modules', 'jdk.incubator.vector')` line):
-
-```groovy
- if (project.hasProperty('noise.bench')) {
- systemProperty('noise.bench', project.property('noise.bench'))
- testLogging { showStandardStreams = true }
- }
-```
-
-Run:
-
-```bash
-./gradlew :core:test --tests 'art.arcane.iris.util.simd.NoiseKernels2DBenchmarkHarness' -Pnoise.bench=true
-```
-
-Capture the `BENCH ... speedup=...` line.
-
-Then CLEAN UP (do not commit either): delete the harness file and remove ONLY the `noise.bench` lines you added from `core/build.gradle` (edit them out by hand — do NOT `git checkout`/`git restore` the file, there may be other uncommitted work). Confirm with `git diff HEAD -- core/build.gradle` showing no diff.
-
----
-
-## Report back to the Mac (this exact info)
-
-1. `os.arch` and `doubleLanes` (4 or 8) from step 1.
-2. Step 2 parity result: the `tests/failures/errors/skipped` counts. (Must be `6/0/0/0`.) If any failure: the failing test name, octave, index, expected vs actual.
-3. Step 3 `BENCH` line: `scalarNsPerCol`, `vectorNsPerCol`, `speedup`.
-4. Confirmation the benchmark harness was deleted and `core/build.gradle` restored to no-diff.
-
-That's it — do NOT wire the kernel into world generation, do NOT commit, do NOT touch any unrelated modified files in the working tree. This is a measurement-only task.
-
----
-
-## What the numbers decide (context for whoever relays this)
-
-- **Parity 6/0/0/0 with 0 skips** → the kernel is bit-exact at this lane width; the determinism gate holds on x86. Required before it can ever be wired into generation.
-- **speedup > 1** (ideally ≥1.5–2× at 4 lanes, more at 8) → confirms the kernel is worth wiring into the live noise pipeline (Phase 2: batch the composite height/biome/cave noise paths, which is the deep part). Noise is ~33% of generation CPU, so a real per-eval win translates to a meaningful pregen/gen throughput gain.
-- **speedup ≤ 1 even here** → the approach doesn't pay even with hardware gather + wide lanes; do not wire it; revisit a different optimization (e.g. the ~14% NMS chunk-write cost) or GPU compute.
diff --git a/tools/simd-bench/README.md b/tools/simd-bench/README.md
new file mode 100644
index 000000000..39d1304e7
--- /dev/null
+++ b/tools/simd-bench/README.md
@@ -0,0 +1,101 @@
+# Iris SIMD Kernel Benchmark
+
+Standalone, portable microbenchmark that measures whether Iris's SIMD (Java
+Vector API) kernels actually beat their scalar equivalents **on the CPU it runs
+on**. The Volmit dev/test machines are Apple Silicon, where the wide-SIMD path
+(4+ double lanes) does not exist. Copy the built artifact to a Windows/x86 box
+(AVX2 = 4 lanes, AVX-512 = 8 lanes) and run it to get real numbers for that CPU.
+
+The six kernel classes are copied verbatim (logic byte-for-byte) from
+`Iris/core/.../util/simd/`. This tool is intentionally a duplicate so it builds
+and runs on its own with no Gradle, no VolmLib, and no Iris on the classpath.
+
+## Requirements
+
+- **JDK 25** (the tool is compiled with `--release 25`).
+- The JDK must ship the `jdk.incubator.vector` incubator module (Temurin,
+ Oracle, and all standard OpenJDK builds do).
+
+## How to run
+
+Windows:
+
+```
+run.bat
+```
+
+macOS / Linux:
+
+```
+./run.sh
+```
+
+Or invoke directly (the `--add-modules` flag is required because the Vector API
+is still an incubator module):
+
+```
+java --add-modules jdk.incubator.vector -jar simd-bench.jar
+```
+
+### Mode flag
+
+By default it runs `both` (scalar and vector head-to-head in one run). You can
+also do a two-run A/B:
+
+```
+run.bat --mode scalar
+run.bat --mode vector
+run.bat --mode both (default)
+```
+
+`--mode=scalar` syntax works too. The correctness cross-check only runs in
+`both` mode (it needs both implementations).
+
+## How to read the output
+
+- **Header** prints the JVM, `os.arch`, CPU count, and the preferred vector
+ width. `DoubleVector pref: N lanes` is the SIMD width for `double` on this CPU
+ (2 on 128-bit NEON, 4 on AVX2, 8 on AVX-512).
+- **noise SIMD gate (aligned && doubleLanes>=4)** mirrors the real profitability
+ gate in Iris's `VectorNoiseKernels2D`. It reports ENABLED on 4+ lane CPUs and
+ DISABLED on 2-lane NEON. **This tool ignores the gate and force-measures the
+ raw vector kernel anyway**, so you see the real number even where Iris would
+ gate SIMD off.
+- **Correctness cross-check** runs each kernel once with both impls on identical
+ input and confirms they agree (roundToInt/noise are bit-exact; `sum` differs
+ only by floating-point reduction order, checked with a relative tolerance).
+ If anything says MISMATCH, do not trust the timing numbers below it.
+- **speedup = scalar ns/op / vector ns/op.** `> 1.0` means SIMD is faster on
+ this CPU; `< 1.0` means SIMD is slower. Verdict column: SIMD FASTER / SIMD
+ SLOWER / NEUTRAL (within ~5%).
+- `ns/op` for the array kernels is one full-array invocation (256 or 1024
+ doubles). For noise it is one 256-element `simplexFractalFBM` call.
+- The trailing **Checksum** lines exist only to keep the JIT from deleting the
+ measured work. Ignore their values.
+
+## Harness notes (why the numbers are trustworthy)
+
+- Each op is warmed up 50,000 times before timing so the JIT has compiled the
+ hot path.
+- Every kernel output is folded into a running checksum (printed at the end) so
+ no measured call is dead-code-eliminated.
+- One input element is perturbed per iteration (a cheap store keyed off the loop
+ counter) so the JIT cannot hoist a "constant" result out of the timing loop.
+ This perturbation is identical for scalar and vector, so the comparison stays
+ fair; it adds a tiny fixed cost to both sides that very slightly compresses the
+ ratio on the cheapest kernels.
+- Each (kernel, impl) is timed over 10 rounds; the **minimum** ns/op is reported
+ (least-noisy statistic for a microbenchmark).
+- Scalar and vector see the same seeded input for a given kernel.
+
+## Caveats
+
+- This is an **isolated-kernel vacuum microbench**, not full worldgen. It says
+ whether the raw kernel is faster on this CPU, not what end-to-end generation
+ throughput will be (cache behavior, allocation, and surrounding code differ in
+ the real engine).
+- **Effort 1 array kernels** (`roundToInt` / `sum` / `max`) run unconditionally
+ in real Iris. **Effort 2 noise** (`simplexFractalFBM`) is currently unwired in
+ Iris and gated to 4+ double lanes; this tool force-measures it regardless.
+- Vector-API auto-vectorization and cost depend heavily on the JDK version and
+ CPU. Run on the actual target hardware; do not extrapolate across machines.
diff --git a/tools/simd-bench/out/simdbench/Bench.class b/tools/simd-bench/out/simdbench/Bench.class
new file mode 100644
index 000000000..45f8e6dff
Binary files /dev/null and b/tools/simd-bench/out/simdbench/Bench.class differ
diff --git a/tools/simd-bench/out/simdbench/NoiseKernels2D.class b/tools/simd-bench/out/simdbench/NoiseKernels2D.class
new file mode 100644
index 000000000..e80d06ef3
Binary files /dev/null and b/tools/simd-bench/out/simdbench/NoiseKernels2D.class differ
diff --git a/tools/simd-bench/out/simdbench/ScalarNoiseKernels2D.class b/tools/simd-bench/out/simdbench/ScalarNoiseKernels2D.class
new file mode 100644
index 000000000..d4e102df2
Binary files /dev/null and b/tools/simd-bench/out/simdbench/ScalarNoiseKernels2D.class differ
diff --git a/tools/simd-bench/out/simdbench/ScalarSimdKernels.class b/tools/simd-bench/out/simdbench/ScalarSimdKernels.class
new file mode 100644
index 000000000..556d95e29
Binary files /dev/null and b/tools/simd-bench/out/simdbench/ScalarSimdKernels.class differ
diff --git a/tools/simd-bench/out/simdbench/SimdKernels.class b/tools/simd-bench/out/simdbench/SimdKernels.class
new file mode 100644
index 000000000..7b1832343
Binary files /dev/null and b/tools/simd-bench/out/simdbench/SimdKernels.class differ
diff --git a/tools/simd-bench/out/simdbench/VectorNoiseKernels2D.class b/tools/simd-bench/out/simdbench/VectorNoiseKernels2D.class
new file mode 100644
index 000000000..58e77d117
Binary files /dev/null and b/tools/simd-bench/out/simdbench/VectorNoiseKernels2D.class differ
diff --git a/tools/simd-bench/out/simdbench/VectorSimdKernels.class b/tools/simd-bench/out/simdbench/VectorSimdKernels.class
new file mode 100644
index 000000000..711cc9c80
Binary files /dev/null and b/tools/simd-bench/out/simdbench/VectorSimdKernels.class differ
diff --git a/tools/simd-bench/run.bat b/tools/simd-bench/run.bat
new file mode 100644
index 000000000..d04890357
--- /dev/null
+++ b/tools/simd-bench/run.bat
@@ -0,0 +1,2 @@
+@echo off
+java --add-modules jdk.incubator.vector -jar simd-bench.jar %*
diff --git a/tools/simd-bench/run.sh b/tools/simd-bench/run.sh
new file mode 100755
index 000000000..3c459122b
--- /dev/null
+++ b/tools/simd-bench/run.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$(dirname "$0")"
+java --add-modules jdk.incubator.vector -jar simd-bench.jar "$@"
diff --git a/tools/simd-bench/simd-bench.jar b/tools/simd-bench/simd-bench.jar
new file mode 100644
index 000000000..faf4a9a3e
Binary files /dev/null and b/tools/simd-bench/simd-bench.jar differ
diff --git a/tools/simd-bench/src/simdbench/Bench.java b/tools/simd-bench/src/simdbench/Bench.java
new file mode 100644
index 000000000..b7b9f0d22
--- /dev/null
+++ b/tools/simd-bench/src/simdbench/Bench.java
@@ -0,0 +1,387 @@
+package simdbench;
+
+import java.util.Locale;
+import java.util.Random;
+import jdk.incubator.vector.DoubleVector;
+import jdk.incubator.vector.LongVector;
+import jdk.incubator.vector.VectorSpecies;
+
+public final class Bench {
+ private static final int WARMUP = 50_000;
+ private static final int ROUNDS = 10;
+ private static final int ARRAY_BATCH = 200_000;
+ private static final int NOISE_LENGTH = 256;
+ private static final long SEED = 1337L;
+ private static final double FREQUENCY = 0.01D;
+ private static final double LACUNARITY = 2.0D;
+ private static final double GAIN = 0.5D;
+ private static final int[] ARRAY_LENGTHS = {256, 1024};
+ private static final int[] OCTAVE_SET = {1, 3, 4, 8};
+
+ private static long longSink = 0L;
+ private static double doubleSink = 0.0D;
+
+ private Bench() {
+ }
+
+ public static void main(String[] args) {
+ String mode = parseMode(args);
+ printHeader(mode);
+
+ SimdKernels scalarArray = new ScalarSimdKernels();
+ SimdKernels vectorArray = new VectorSimdKernels();
+ NoiseKernels2D scalarNoise = new ScalarNoiseKernels2D();
+ NoiseKernels2D vectorNoise = new VectorNoiseKernels2D();
+
+ System.out.println("Array vector impl: " + vectorArray.describe());
+ System.out.println("Noise vector impl: " + vectorNoise.describe());
+
+ if (mode.equals("both")) {
+ runCorrectness(scalarArray, vectorArray, scalarNoise, vectorNoise);
+ }
+
+ runArrayBenchmarks(mode, scalarArray, vectorArray);
+ runNoiseBenchmarks(mode, scalarNoise, vectorNoise);
+
+ System.out.println();
+ System.out.println("Checksum (guards against dead-code elimination, ignore the values):");
+ System.out.println(" longSink=" + longSink);
+ System.out.println(" doubleSink=" + doubleSink);
+ }
+
+ private static String parseMode(String[] args) {
+ String mode = "both";
+ for (int i = 0; i < args.length; i++) {
+ if (args[i].equals("--mode") && i + 1 < args.length) {
+ mode = args[i + 1].toLowerCase(Locale.ROOT);
+ } else if (args[i].startsWith("--mode=")) {
+ mode = args[i].substring("--mode=".length()).toLowerCase(Locale.ROOT);
+ }
+ }
+ if (!mode.equals("scalar") && !mode.equals("vector") && !mode.equals("both")) {
+ System.out.println("Unknown mode '" + mode + "', falling back to 'both'.");
+ mode = "both";
+ }
+ return mode;
+ }
+
+ private static void printHeader(String mode) {
+ VectorSpecies doubleSpecies = DoubleVector.SPECIES_PREFERRED;
+ VectorSpecies longSpecies = LongVector.SPECIES_PREFERRED;
+ int doubleLanes = doubleSpecies.length();
+ int longLanes = longSpecies.length();
+ boolean aligned = doubleLanes == longLanes;
+ boolean gate = aligned && doubleLanes >= 4;
+ System.out.println("=== Iris SIMD Kernel Benchmark ===");
+ System.out.println("Java version: " + System.getProperty("java.version"));
+ System.out.println("Java vendor: " + System.getProperty("java.vendor"));
+ System.out.println("os.arch: " + System.getProperty("os.arch"));
+ System.out.println("os.name: " + System.getProperty("os.name"));
+ System.out.println("availableProcessors: " + Runtime.getRuntime().availableProcessors());
+ System.out.println("DoubleVector pref: " + doubleLanes + " lanes, " + doubleSpecies.vectorShape());
+ System.out.println("LongVector pref: " + longLanes + " lanes, " + longSpecies.vectorShape());
+ System.out.println("noise SIMD gate (aligned && doubleLanes>=4): " + (gate ? "ENABLED" : "DISABLED") + " on this CPU");
+ System.out.println("Mode: " + mode + " (WARMUP=" + WARMUP + ", ROUNDS=" + ROUNDS + ", reporting MIN ns/op)");
+ }
+
+ private static void runCorrectness(SimdKernels scalarArray, SimdKernels vectorArray,
+ NoiseKernels2D scalarNoise, NoiseKernels2D vectorNoise) {
+ System.out.println();
+ System.out.println("--- Correctness cross-check (scalar vs vector on identical input) ---");
+ for (int li = 0; li < ARRAY_LENGTHS.length; li++) {
+ int length = ARRAY_LENGTHS[li];
+ double[] data = makeArray(length, 777L);
+
+ int[] targetScalar = new int[length];
+ int[] targetVector = new int[length];
+ scalarArray.roundToInt(data, targetScalar, length);
+ vectorArray.roundToInt(data, targetVector, length);
+ int mismatches = 0;
+ for (int i = 0; i < length; i++) {
+ if (targetScalar[i] != targetVector[i]) {
+ mismatches++;
+ }
+ }
+ System.out.printf(" roundToInt len=%-5d %s%n", length,
+ mismatches == 0 ? "MATCH" : ("MISMATCH x" + mismatches));
+
+ double sumScalar = scalarArray.sum(data, length);
+ double sumVector = vectorArray.sum(data, length);
+ System.out.printf(" sum len=%-5d %s (scalar=%.6f vector=%.6f, |rel diff|=%.2e)%n",
+ length, relClose(sumScalar, sumVector, 1.0E-9D) ? "MATCH" : "MISMATCH",
+ sumScalar, sumVector, relDiff(sumScalar, sumVector));
+
+ double maxScalar = scalarArray.max(data, length);
+ double maxVector = vectorArray.max(data, length);
+ System.out.printf(" max len=%-5d %s (scalar=%.6f vector=%.6f)%n",
+ length, maxScalar == maxVector ? "MATCH" : "MISMATCH", maxScalar, maxVector);
+ }
+
+ double[] xs = makeCoords(NOISE_LENGTH, 0.0D, 1.0D);
+ double[] zs = makeCoords(NOISE_LENGTH, 4096.0D, 1.0D);
+ for (int oi = 0; oi < OCTAVE_SET.length; oi++) {
+ int octaves = OCTAVE_SET[oi];
+ double fractalBounding = calcFractalBounding(octaves, GAIN);
+ double[] outScalar = new double[NOISE_LENGTH];
+ double[] outVector = new double[NOISE_LENGTH];
+ scalarNoise.simplexFractalFBM(SEED, octaves, FREQUENCY, LACUNARITY, GAIN, fractalBounding, xs, zs, outScalar, NOISE_LENGTH);
+ vectorNoise.simplexFractalFBM(SEED, octaves, FREQUENCY, LACUNARITY, GAIN, fractalBounding, xs, zs, outVector, NOISE_LENGTH);
+ double maxAbs = 0.0D;
+ for (int i = 0; i < NOISE_LENGTH; i++) {
+ double diff = Math.abs(outScalar[i] - outVector[i]);
+ if (diff > maxAbs) {
+ maxAbs = diff;
+ }
+ }
+ System.out.printf(" noise oct=%-2d %s (max |diff|=%.2e)%n",
+ octaves, maxAbs < 1.0E-9D ? "MATCH" : "MISMATCH", maxAbs);
+ }
+ }
+
+ private static void runArrayBenchmarks(String mode, SimdKernels scalar, SimdKernels vector) {
+ System.out.println();
+ System.out.println("--- Array kernels (one op = one full-array invocation, batch=" + ARRAY_BATCH + ") ---");
+ System.out.printf("%-12s %-6s %14s %14s %9s %s%n",
+ "kernel", "len", "scalar ns/op", "vector ns/op", "speedup", "verdict");
+ String[] kernels = {"roundToInt", "sum", "max"};
+ for (int li = 0; li < ARRAY_LENGTHS.length; li++) {
+ int length = ARRAY_LENGTHS[li];
+ for (int ki = 0; ki < kernels.length; ki++) {
+ String kernel = kernels[ki];
+ double scalarNs = Double.NaN;
+ double vectorNs = Double.NaN;
+ if (!mode.equals("vector")) {
+ scalarNs = timeArrayKernel(kernel, scalar, length);
+ }
+ if (!mode.equals("scalar")) {
+ vectorNs = timeArrayKernel(kernel, vector, length);
+ }
+ printRow(kernel, Integer.toString(length), scalarNs, vectorNs);
+ }
+ }
+ }
+
+ private static void runNoiseBenchmarks(String mode, NoiseKernels2D scalar, NoiseKernels2D vector) {
+ System.out.println();
+ System.out.println("--- Noise kernel simplexFractalFBM (one op = one 256-element invocation) ---");
+ System.out.printf("%-10s %-8s %14s %14s %9s %s%n",
+ "octaves", "batch", "scalar ns/op", "vector ns/op", "speedup", "verdict");
+ int mask = NOISE_LENGTH - 1;
+ for (int oi = 0; oi < OCTAVE_SET.length; oi++) {
+ int octaves = OCTAVE_SET[oi];
+ int batch = noiseBatch(octaves);
+ double scalarNs = Double.NaN;
+ double vectorNs = Double.NaN;
+ if (!mode.equals("vector")) {
+ scalarNs = timeNoise(scalar, octaves, mask, batch);
+ }
+ if (!mode.equals("scalar")) {
+ vectorNs = timeNoise(vector, octaves, mask, batch);
+ }
+ printNoiseRow(octaves, batch, scalarNs, vectorNs);
+ }
+ }
+
+ private static double timeArrayKernel(String kernel, SimdKernels impl, int length) {
+ double[] source = makeArray(length, 20260701L);
+ int[] target = new int[length];
+ int mask = length - 1;
+ return switch (kernel) {
+ case "roundToInt" -> timeRoundToInt(impl, source, target, length, mask);
+ case "sum" -> timeSum(impl, source, length, mask);
+ case "max" -> timeMax(impl, source, length, mask);
+ default -> throw new IllegalArgumentException(kernel);
+ };
+ }
+
+ private static double timeRoundToInt(SimdKernels impl, double[] source, int[] target, int length, int mask) {
+ long warm = 0L;
+ for (int b = 0; b < WARMUP; b++) {
+ source[b & mask] = perturb(b);
+ impl.roundToInt(source, target, length);
+ warm += target[b & mask];
+ }
+ longSink += warm;
+ double best = Double.MAX_VALUE;
+ for (int r = 0; r < ROUNDS; r++) {
+ long localSink = 0L;
+ long start = System.nanoTime();
+ for (int b = 0; b < ARRAY_BATCH; b++) {
+ source[b & mask] = perturb(b);
+ impl.roundToInt(source, target, length);
+ localSink += target[b & mask];
+ }
+ long elapsed = System.nanoTime() - start;
+ longSink += localSink;
+ double nsPerOp = (double) elapsed / (double) ARRAY_BATCH;
+ if (nsPerOp < best) {
+ best = nsPerOp;
+ }
+ }
+ return best;
+ }
+
+ private static double timeSum(SimdKernels impl, double[] source, int length, int mask) {
+ double warm = 0.0D;
+ for (int b = 0; b < WARMUP; b++) {
+ source[b & mask] = perturb(b);
+ warm += impl.sum(source, length);
+ }
+ doubleSink += warm;
+ double best = Double.MAX_VALUE;
+ for (int r = 0; r < ROUNDS; r++) {
+ double localSink = 0.0D;
+ long start = System.nanoTime();
+ for (int b = 0; b < ARRAY_BATCH; b++) {
+ source[b & mask] = perturb(b);
+ localSink += impl.sum(source, length);
+ }
+ long elapsed = System.nanoTime() - start;
+ doubleSink += localSink;
+ double nsPerOp = (double) elapsed / (double) ARRAY_BATCH;
+ if (nsPerOp < best) {
+ best = nsPerOp;
+ }
+ }
+ return best;
+ }
+
+ private static double timeMax(SimdKernels impl, double[] source, int length, int mask) {
+ double warm = 0.0D;
+ for (int b = 0; b < WARMUP; b++) {
+ source[b & mask] = perturb(b);
+ warm += impl.max(source, length);
+ }
+ doubleSink += warm;
+ double best = Double.MAX_VALUE;
+ for (int r = 0; r < ROUNDS; r++) {
+ double localSink = 0.0D;
+ long start = System.nanoTime();
+ for (int b = 0; b < ARRAY_BATCH; b++) {
+ source[b & mask] = perturb(b);
+ localSink += impl.max(source, length);
+ }
+ long elapsed = System.nanoTime() - start;
+ doubleSink += localSink;
+ double nsPerOp = (double) elapsed / (double) ARRAY_BATCH;
+ if (nsPerOp < best) {
+ best = nsPerOp;
+ }
+ }
+ return best;
+ }
+
+ private static double timeNoise(NoiseKernels2D impl, int octaves, int mask, int batch) {
+ double[] xs = makeCoords(NOISE_LENGTH, 0.0D, 1.0D);
+ double[] zs = makeCoords(NOISE_LENGTH, 4096.0D, 1.0D);
+ double[] out = new double[NOISE_LENGTH];
+ double fractalBounding = calcFractalBounding(octaves, GAIN);
+ double warm = 0.0D;
+ for (int b = 0; b < WARMUP; b++) {
+ xs[b & mask] = 0.5D * (double) (b & 1023);
+ impl.simplexFractalFBM(SEED, octaves, FREQUENCY, LACUNARITY, GAIN, fractalBounding, xs, zs, out, NOISE_LENGTH);
+ warm += out[b & mask];
+ }
+ doubleSink += warm;
+ double best = Double.MAX_VALUE;
+ for (int r = 0; r < ROUNDS; r++) {
+ double localSink = 0.0D;
+ long start = System.nanoTime();
+ for (int b = 0; b < batch; b++) {
+ xs[b & mask] = 0.5D * (double) (b & 1023);
+ impl.simplexFractalFBM(SEED, octaves, FREQUENCY, LACUNARITY, GAIN, fractalBounding, xs, zs, out, NOISE_LENGTH);
+ localSink += out[b & mask];
+ }
+ long elapsed = System.nanoTime() - start;
+ doubleSink += localSink;
+ double nsPerOp = (double) elapsed / (double) batch;
+ if (nsPerOp < best) {
+ best = nsPerOp;
+ }
+ }
+ return best;
+ }
+
+ private static double perturb(int b) {
+ return (double) ((b * 2654435761L) & 1023L) - 256.0D;
+ }
+
+ private static int noiseBatch(int octaves) {
+ return Math.max(4_000, 50_000 / octaves);
+ }
+
+ private static double calcFractalBounding(int octaves, double gain) {
+ double amp = gain;
+ double ampFractal = 1.0D;
+ for (int i = 1; i < octaves; i++) {
+ ampFractal += amp;
+ amp *= gain;
+ }
+ return 1.0D / ampFractal;
+ }
+
+ private static double[] makeArray(int length, long seed) {
+ Random random = new Random(seed);
+ double[] data = new double[length];
+ for (int i = 0; i < length; i++) {
+ data[i] = random.nextDouble() * 512.0D - 64.0D;
+ }
+ return data;
+ }
+
+ private static double[] makeCoords(int length, double origin, double step) {
+ double[] data = new double[length];
+ for (int i = 0; i < length; i++) {
+ data[i] = origin + (double) i * step;
+ }
+ return data;
+ }
+
+ private static void printRow(String kernel, String length, double scalarNs, double vectorNs) {
+ String scalarText = Double.isNaN(scalarNs) ? "-" : String.format(Locale.ROOT, "%.3f", scalarNs);
+ String vectorText = Double.isNaN(vectorNs) ? "-" : String.format(Locale.ROOT, "%.3f", vectorNs);
+ String speedupText = "-";
+ String verdict = "-";
+ if (!Double.isNaN(scalarNs) && !Double.isNaN(vectorNs) && vectorNs > 0.0D) {
+ double speedup = scalarNs / vectorNs;
+ speedupText = String.format(Locale.ROOT, "%.2fx", speedup);
+ verdict = verdictFor(speedup);
+ }
+ System.out.printf("%-12s %-6s %14s %14s %9s %s%n", kernel, length, scalarText, vectorText, speedupText, verdict);
+ }
+
+ private static void printNoiseRow(int octaves, int batch, double scalarNs, double vectorNs) {
+ String scalarText = Double.isNaN(scalarNs) ? "-" : String.format(Locale.ROOT, "%.1f", scalarNs);
+ String vectorText = Double.isNaN(vectorNs) ? "-" : String.format(Locale.ROOT, "%.1f", vectorNs);
+ String speedupText = "-";
+ String verdict = "-";
+ if (!Double.isNaN(scalarNs) && !Double.isNaN(vectorNs) && vectorNs > 0.0D) {
+ double speedup = scalarNs / vectorNs;
+ speedupText = String.format(Locale.ROOT, "%.2fx", speedup);
+ verdict = verdictFor(speedup);
+ }
+ System.out.printf("%-10d %-8d %14s %14s %9s %s%n", octaves, batch, scalarText, vectorText, speedupText, verdict);
+ }
+
+ private static String verdictFor(double speedup) {
+ if (speedup > 1.05D) {
+ return "SIMD FASTER";
+ }
+ if (speedup < 0.95D) {
+ return "SIMD SLOWER";
+ }
+ return "NEUTRAL";
+ }
+
+ private static double relDiff(double a, double b) {
+ double denom = Math.max(Math.abs(a), Math.abs(b));
+ if (denom == 0.0D) {
+ return 0.0D;
+ }
+ return Math.abs(a - b) / denom;
+ }
+
+ private static boolean relClose(double a, double b, double tol) {
+ return relDiff(a, b) <= tol;
+ }
+}
diff --git a/tools/simd-bench/src/simdbench/NoiseKernels2D.java b/tools/simd-bench/src/simdbench/NoiseKernels2D.java
new file mode 100644
index 000000000..9ae6320cc
--- /dev/null
+++ b/tools/simd-bench/src/simdbench/NoiseKernels2D.java
@@ -0,0 +1,8 @@
+package simdbench;
+
+public interface NoiseKernels2D {
+ String describe();
+
+ void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain,
+ double fractalBounding, double[] xs, double[] zs, double[] out, int length);
+}
diff --git a/tools/simd-bench/src/simdbench/ScalarNoiseKernels2D.java b/tools/simd-bench/src/simdbench/ScalarNoiseKernels2D.java
new file mode 100644
index 000000000..f02a21e1e
--- /dev/null
+++ b/tools/simd-bench/src/simdbench/ScalarNoiseKernels2D.java
@@ -0,0 +1,99 @@
+package simdbench;
+
+public final class ScalarNoiseKernels2D implements NoiseKernels2D {
+ static final double F2 = 0.5D * (Math.sqrt(3.0D) - 1.0D);
+ static final double G2 = (3.0D - Math.sqrt(3.0D)) / 6.0D;
+ static final long X_PRIME = 1619L;
+ static final long Y_PRIME = 31337L;
+ static final double[] GRAD_2D = {-1D, -1D, 1D, -1D, -1D, 1D, 1D, 1D, 0D, -1D, -1D, 0D, 0D, 1D, 1D, 0D};
+
+ @Override
+ public String describe() {
+ return "scalar";
+ }
+
+ static long fastFloor(double f) {
+ return f >= 0D ? (long) f : (long) f - 1L;
+ }
+
+ static double gradCoord2D(long seed, long x, long y, double xd, double yd) {
+ long hash = seed;
+ hash ^= X_PRIME * x;
+ hash ^= Y_PRIME * y;
+ hash = hash * hash * hash * 60493L;
+ hash = (hash >> 13) ^ hash;
+ int gradientIndex = ((int) hash & 7) << 1;
+ return (xd * GRAD_2D[gradientIndex]) + (yd * GRAD_2D[gradientIndex + 1]);
+ }
+
+ static double singleSimplex(long seed, double x, double y) {
+ double t = (x + y) * F2;
+ long i = fastFloor(x + t);
+ long j = fastFloor(y + t);
+ t = (i + j) * G2;
+ double x0 = x - (i - t);
+ double y0 = y - (j - t);
+ long i1;
+ long j1;
+ if (x0 > y0) {
+ i1 = 1L;
+ j1 = 0L;
+ } else {
+ i1 = 0L;
+ j1 = 1L;
+ }
+ double x1 = x0 - i1 + G2;
+ double y1 = y0 - j1 + G2;
+ double x2 = x0 - 1D + (2D * G2);
+ double y2 = y0 - 1D + (2D * G2);
+ double n0;
+ double n1;
+ double n2;
+ double a = 0.5D - x0 * x0 - y0 * y0;
+ if (a < 0D) {
+ n0 = 0D;
+ } else {
+ a *= a;
+ n0 = a * a * gradCoord2D(seed, i, j, x0, y0);
+ }
+ double b = 0.5D - x1 * x1 - y1 * y1;
+ if (b < 0D) {
+ n1 = 0D;
+ } else {
+ b *= b;
+ n1 = b * b * gradCoord2D(seed, i + i1, j + j1, x1, y1);
+ }
+ double c = 0.5D - x2 * x2 - y2 * y2;
+ if (c < 0D) {
+ n2 = 0D;
+ } else {
+ c *= c;
+ n2 = c * c * gradCoord2D(seed, i + 1L, j + 1L, x2, y2);
+ }
+ return 50D * (n0 + n1 + n2);
+ }
+
+ static double simplexFractalFBMScalar(long seed, int octaves, double frequency, double lacunarity, double gain,
+ double fractalBounding, double xIn, double yIn) {
+ double x = xIn * frequency;
+ double y = yIn * frequency;
+ long s = seed;
+ double sum = singleSimplex(s, x, y);
+ double amp = 1D;
+ for (int o = 1; o < octaves; o++) {
+ x *= lacunarity;
+ y *= lacunarity;
+ amp *= gain;
+ sum += singleSimplex(++s, x, y) * amp;
+ }
+ return sum * fractalBounding;
+ }
+
+ @Override
+ public void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain,
+ double fractalBounding, double[] xs, double[] zs, double[] out, int length) {
+ for (int k = 0; k < length; k++) {
+ out[k] = simplexFractalFBMScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs[k], zs[k]);
+ }
+ }
+}
diff --git a/tools/simd-bench/src/simdbench/ScalarSimdKernels.java b/tools/simd-bench/src/simdbench/ScalarSimdKernels.java
new file mode 100644
index 000000000..ef577e051
--- /dev/null
+++ b/tools/simd-bench/src/simdbench/ScalarSimdKernels.java
@@ -0,0 +1,37 @@
+package simdbench;
+
+public final class ScalarSimdKernels implements SimdKernels {
+ @Override
+ public String describe() {
+ return "scalar";
+ }
+
+ @Override
+ public void roundToInt(double[] source, int[] target, int length) {
+ for (int index = 0; index < length; index++) {
+ target[index] = (int) Math.round(source[index]);
+ }
+ }
+
+ @Override
+ public double sum(double[] values, int length) {
+ double total = 0D;
+ for (int index = 0; index < length; index++) {
+ total += values[index];
+ }
+
+ return total;
+ }
+
+ @Override
+ public double max(double[] values, int length) {
+ double best = Double.NEGATIVE_INFINITY;
+ for (int index = 0; index < length; index++) {
+ if (values[index] > best) {
+ best = values[index];
+ }
+ }
+
+ return best;
+ }
+}
diff --git a/tools/simd-bench/src/simdbench/SimdKernels.java b/tools/simd-bench/src/simdbench/SimdKernels.java
new file mode 100644
index 000000000..f73d6fd00
--- /dev/null
+++ b/tools/simd-bench/src/simdbench/SimdKernels.java
@@ -0,0 +1,11 @@
+package simdbench;
+
+public interface SimdKernels {
+ String describe();
+
+ void roundToInt(double[] source, int[] target, int length);
+
+ double sum(double[] values, int length);
+
+ double max(double[] values, int length);
+}
diff --git a/tools/simd-bench/src/simdbench/VectorNoiseKernels2D.java b/tools/simd-bench/src/simdbench/VectorNoiseKernels2D.java
new file mode 100644
index 000000000..fae195856
--- /dev/null
+++ b/tools/simd-bench/src/simdbench/VectorNoiseKernels2D.java
@@ -0,0 +1,129 @@
+package simdbench;
+
+import jdk.incubator.vector.DoubleVector;
+import jdk.incubator.vector.LongVector;
+import jdk.incubator.vector.VectorMask;
+import jdk.incubator.vector.VectorOperators;
+import jdk.incubator.vector.VectorSpecies;
+
+public final class VectorNoiseKernels2D implements NoiseKernels2D {
+ private static final VectorSpecies DS = DoubleVector.SPECIES_PREFERRED;
+ private static final VectorSpecies LS = LongVector.SPECIES_PREFERRED;
+ private static final boolean ALIGNED = DS.length() == LS.length();
+ private static final int MIN_PROFITABLE_LANES = 4;
+ private static final boolean PROFITABLE = ALIGNED && DS.length() >= MIN_PROFITABLE_LANES;
+ private static final double[] GRAD_X = {-1D, 1D, -1D, 1D, 0D, -1D, 0D, 1D};
+ private static final double[] GRAD_Y = {-1D, -1D, 1D, 1D, -1D, 0D, 1D, 0D};
+ private static final double F2 = ScalarNoiseKernels2D.F2;
+ private static final double G2 = ScalarNoiseKernels2D.G2;
+ private static final long X_PRIME = ScalarNoiseKernels2D.X_PRIME;
+ private static final long Y_PRIME = ScalarNoiseKernels2D.Y_PRIME;
+ private static final ThreadLocal LONG_SCRATCH = ThreadLocal.withInitial(() -> new long[LS.length()]);
+
+ public static boolean lanesAligned() {
+ return ALIGNED;
+ }
+
+ public static boolean profitable() {
+ return PROFITABLE;
+ }
+
+ @Override
+ public String describe() {
+ return DS.length() + "x64 lanes, " + DS.vectorShape();
+ }
+
+ private static LongVector floorToLong(DoubleVector f) {
+ LongVector truncated = (LongVector) f.convertShape(VectorOperators.D2L, LS, 0);
+ VectorMask negative = f.compare(VectorOperators.LT, 0D).cast(LS);
+ return truncated.sub(1L, negative);
+ }
+
+ private static DoubleVector toDouble(LongVector v) {
+ return (DoubleVector) v.convertShape(VectorOperators.L2D, DS, 0);
+ }
+
+ private static DoubleVector gradCoord(long seed, LongVector i, LongVector j, DoubleVector xd, DoubleVector yd,
+ int[] idxScratch) {
+ LongVector hash = LongVector.broadcast(LS, seed)
+ .lanewise(VectorOperators.XOR, i.mul(X_PRIME))
+ .lanewise(VectorOperators.XOR, j.mul(Y_PRIME));
+ hash = hash.mul(hash).mul(hash).mul(60493L);
+ LongVector shifted = hash.lanewise(VectorOperators.ASHR, 13);
+ hash = shifted.lanewise(VectorOperators.XOR, hash);
+ LongVector idx = hash.lanewise(VectorOperators.AND, 7L);
+ long[] tmp = LONG_SCRATCH.get();
+ idx.intoArray(tmp, 0);
+ for (int l = 0; l < idxScratch.length; l++) {
+ idxScratch[l] = (int) tmp[l];
+ }
+ DoubleVector gx = DoubleVector.fromArray(DS, GRAD_X, 0, idxScratch, 0);
+ DoubleVector gy = DoubleVector.fromArray(DS, GRAD_Y, 0, idxScratch, 0);
+ return xd.mul(gx).add(yd.mul(gy));
+ }
+
+ private static DoubleVector corner(DoubleVector xk, DoubleVector yk, DoubleVector grad) {
+ DoubleVector t = DoubleVector.broadcast(DS, 0.5D).sub(xk.mul(xk)).sub(yk.mul(yk));
+ VectorMask negative = t.compare(VectorOperators.LT, 0D);
+ DoubleVector t2 = t.mul(t);
+ DoubleVector t4 = t2.mul(t2);
+ return t4.mul(grad).blend(0D, negative);
+ }
+
+ private static DoubleVector singleSimplexVector(long seed, DoubleVector x, DoubleVector y, int[] idxScratch) {
+ DoubleVector t = x.add(y).mul(F2);
+ LongVector i = floorToLong(x.add(t));
+ LongVector j = floorToLong(y.add(t));
+ DoubleVector skew = toDouble(i.add(j)).mul(G2);
+ DoubleVector x0 = x.sub(toDouble(i).sub(skew));
+ DoubleVector y0 = y.sub(toDouble(j).sub(skew));
+ VectorMask xGreater = x0.compare(VectorOperators.GT, y0);
+ VectorMask xGreaterL = xGreater.cast(LS);
+ LongVector i1 = LongVector.zero(LS).blend(1L, xGreaterL);
+ LongVector j1 = LongVector.broadcast(LS, 1L).blend(0L, xGreaterL);
+ DoubleVector x1 = x0.sub(toDouble(i1)).add(G2);
+ DoubleVector y1 = y0.sub(toDouble(j1)).add(G2);
+ DoubleVector x2 = x0.sub(1D).add(2D * G2);
+ DoubleVector y2 = y0.sub(1D).add(2D * G2);
+ DoubleVector n0 = corner(x0, y0, gradCoord(seed, i, j, x0, y0, idxScratch));
+ DoubleVector n1 = corner(x1, y1, gradCoord(seed, i.add(i1), j.add(j1), x1, y1, idxScratch));
+ DoubleVector n2 = corner(x2, y2, gradCoord(seed, i.add(1L), j.add(1L), x2, y2, idxScratch));
+ return n0.add(n1).add(n2).mul(50D);
+ }
+
+ @Override
+ public void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain,
+ double fractalBounding, double[] xs, double[] zs, double[] out, int length) {
+ if (!ALIGNED) {
+ tailScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs, zs, out, 0, length);
+ return;
+ }
+ int lanes = DS.length();
+ int bound = DS.loopBound(length);
+ int[] idxScratch = new int[lanes];
+ int k = 0;
+ for (; k < bound; k += lanes) {
+ DoubleVector x = DoubleVector.fromArray(DS, xs, k).mul(frequency);
+ DoubleVector y = DoubleVector.fromArray(DS, zs, k).mul(frequency);
+ long s = seed;
+ DoubleVector sum = singleSimplexVector(s, x, y, idxScratch);
+ double amp = 1D;
+ for (int o = 1; o < octaves; o++) {
+ x = x.mul(lacunarity);
+ y = y.mul(lacunarity);
+ amp *= gain;
+ sum = sum.add(singleSimplexVector(++s, x, y, idxScratch).mul(amp));
+ }
+ sum.mul(fractalBounding).intoArray(out, k);
+ }
+ tailScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs, zs, out, k, length);
+ }
+
+ private static void tailScalar(long seed, int octaves, double frequency, double lacunarity, double gain,
+ double fractalBounding, double[] xs, double[] zs, double[] out, int from, int length) {
+ for (int k = from; k < length; k++) {
+ out[k] = ScalarNoiseKernels2D.simplexFractalFBMScalar(seed, octaves, frequency, lacunarity, gain,
+ fractalBounding, xs[k], zs[k]);
+ }
+ }
+}
diff --git a/tools/simd-bench/src/simdbench/VectorSimdKernels.java b/tools/simd-bench/src/simdbench/VectorSimdKernels.java
new file mode 100644
index 000000000..2c7b41143
--- /dev/null
+++ b/tools/simd-bench/src/simdbench/VectorSimdKernels.java
@@ -0,0 +1,74 @@
+package simdbench;
+
+import jdk.incubator.vector.DoubleVector;
+import jdk.incubator.vector.IntVector;
+import jdk.incubator.vector.VectorMask;
+import jdk.incubator.vector.VectorOperators;
+import jdk.incubator.vector.VectorShape;
+import jdk.incubator.vector.VectorSpecies;
+
+public final class VectorSimdKernels implements SimdKernels {
+ private static final VectorSpecies DOUBLE_SPECIES = DoubleVector.SPECIES_PREFERRED;
+ private static final VectorSpecies INT_SPECIES = VectorSpecies.of(int.class, VectorShape.forBitSize(DOUBLE_SPECIES.length() * Integer.SIZE));
+
+ @Override
+ public String describe() {
+ return DOUBLE_SPECIES.length() + "x64-bit lanes, " + DOUBLE_SPECIES.vectorShape();
+ }
+
+ @Override
+ public void roundToInt(double[] source, int[] target, int length) {
+ int lanes = DOUBLE_SPECIES.length();
+ int bound = DOUBLE_SPECIES.loopBound(length);
+ int index = 0;
+ for (; index < bound; index += lanes) {
+ DoubleVector shifted = DoubleVector.fromArray(DOUBLE_SPECIES, source, index).add(0.5D);
+ IntVector truncated = (IntVector) shifted.convertShape(VectorOperators.D2I, INT_SPECIES, 0);
+ DoubleVector truncatedBack = (DoubleVector) truncated.convertShape(VectorOperators.I2D, DOUBLE_SPECIES, 0);
+ VectorMask needsDecrement = shifted.lt(truncatedBack).cast(INT_SPECIES);
+ truncated.sub(1, needsDecrement).intoArray(target, index);
+ }
+
+ for (; index < length; index++) {
+ target[index] = (int) Math.round(source[index]);
+ }
+ }
+
+ @Override
+ public double sum(double[] values, int length) {
+ int lanes = DOUBLE_SPECIES.length();
+ int bound = DOUBLE_SPECIES.loopBound(length);
+ DoubleVector accumulator = DoubleVector.zero(DOUBLE_SPECIES);
+ int index = 0;
+ for (; index < bound; index += lanes) {
+ accumulator = accumulator.add(DoubleVector.fromArray(DOUBLE_SPECIES, values, index));
+ }
+
+ double total = accumulator.reduceLanes(VectorOperators.ADD);
+ for (; index < length; index++) {
+ total += values[index];
+ }
+
+ return total;
+ }
+
+ @Override
+ public double max(double[] values, int length) {
+ int lanes = DOUBLE_SPECIES.length();
+ int bound = DOUBLE_SPECIES.loopBound(length);
+ DoubleVector accumulator = DoubleVector.broadcast(DOUBLE_SPECIES, Double.NEGATIVE_INFINITY);
+ int index = 0;
+ for (; index < bound; index += lanes) {
+ accumulator = accumulator.max(DoubleVector.fromArray(DOUBLE_SPECIES, values, index));
+ }
+
+ double best = accumulator.reduceLanes(VectorOperators.MAX);
+ for (; index < length; index++) {
+ if (values[index] > best) {
+ best = values[index];
+ }
+ }
+
+ return best;
+ }
+}