diff --git a/.gitignore b/.gitignore index 653ba488e..e6a873c31 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,8 @@ BUILDS/ .compatwt/ .worktrees/ + +/VolmLib/ +/docs/ +/.perf/ +/.codegraph/ diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java index 905f4ff78..519f63ab1 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java @@ -54,6 +54,7 @@ import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.noise.FractalBillowSimplexNoise; import art.arcane.volmlib.util.matter.MatterStructurePOI; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; @@ -312,7 +313,8 @@ public class IrisEngine implements Engine { } catch (Throwable e) { IrisLogging.reportError(e); } - IrisLogging.notice("Engine init: " + requiredTarget.getWorld().name() + "/" + requiredTarget.getDimension().getLoadKey() + " seed=" + getSeedManager().getSeed()); + IrisLogging.notice("Engine init: " + requiredTarget.getWorld().name() + "/" + requiredTarget.getDimension().getLoadKey() + + " seed=" + getSeedManager().getSeed() + " billow2D=" + FractalBillowSimplexNoise.nativeBackendStatus()); _t0 = M.ms(); phaseStartedAt = System.nanoTime(); EngineRuntime initialRuntime = runtimeBuilder.buildRuntime(cacheIdentity); diff --git a/probe/build.gradle b/probe/build.gradle index 75fb1ca1a..d9dbc22ec 100644 --- a/probe/build.gradle +++ b/probe/build.gradle @@ -154,10 +154,24 @@ tasks.register('terrain3DProbe', JavaExec) { tasks.register('genProbe', JavaExec) { group = 'verification' - description = 'Constructs a real Iris engine over a real pack and generates chunks into buffers on a Bukkit-free JVM.' + description = 'Constructs a real Iris engine over a real pack and verifies generation hashes and optional native backend usage.' mainClass = 'art.arcane.iris.probe.GenerationProbe' classpath = sourceSets.main.runtimeClasspath jvmArgs('--add-modules', 'jdk.incubator.vector') + minHeapSize = '2g' + maxHeapSize = '6g' + if (providers.gradleProperty('probeNativeAccess').orElse('false').get().toBoolean()) { + jvmArgs('--enable-native-access=ALL-UNNAMED') + } + if (providers.gradleProperty('probeNativeBillow').isPresent()) { + systemProperty('volmlib.noise.nativeBillow', providers.gradleProperty('probeNativeBillow').get()) + } + systemProperty('volmlib.noise.nativeBillowDiagnostics', providers.gradleProperty('probeNativeDiagnostics').orElse('false').get()) + systemProperty('iris.probe.expectedBackend', providers.gradleProperty('probeExpectedBackend').orElse('').get()) + systemProperty('iris.probe.requireNativeSamples', providers.gradleProperty('probeRequireNativeSamples').orElse('false').get()) + if (providers.gradleProperty('probeNativeLibrary').isPresent()) { + systemProperty('volmlib.noise.nativeBillowLibrary', providers.gradleProperty('probeNativeLibrary').get()) + } dependsOn(':core:compileJava') doFirst { if (!probePack.isPresent() || probePack.get().isBlank()) { @@ -174,7 +188,8 @@ tasks.register('genProbe', JavaExec) { probeCenterChunkX.get(), probeCenterChunkZ.get(), probeMulticore.get(), - probeStudio.get()) + probeStudio.get(), + probeSeed.orElse('1337').get()) } } diff --git a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java index de492e006..c14475859 100644 --- a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java @@ -39,6 +39,7 @@ import art.arcane.iris.spi.IrisServices; import art.arcane.volmlib.nativelib.terrain.NativeBiome; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.util.hunk.Hunk; +import art.arcane.volmlib.util.noise.FractalBillowSimplexNoise; import java.io.File; import java.nio.charset.StandardCharsets; @@ -58,7 +59,6 @@ import java.util.concurrent.ExecutorService; import java.util.stream.Stream; public final class GenerationProbe { - private static final long SEED = 1337L; private static final List REPORTED = Collections.synchronizedList(new ArrayList<>()); private static final class InertPreservation implements PreservationRegistry { @@ -126,7 +126,7 @@ public final class GenerationProbe { } record ProbeConfiguration(File packSource, String dimensionKey, int warmupChunks, int measuredChunks, - int centerChunkX, int centerChunkZ, boolean multicore, boolean studio) { + int centerChunkX, int centerChunkZ, boolean multicore, boolean studio, long seed) { ProbeConfiguration { if (packSource == null) { throw new IllegalArgumentException("Pack folder is required."); @@ -143,8 +143,8 @@ public final class GenerationProbe { } static ProbeConfiguration parse(String[] args) { - if (args.length != 8) { - throw new IllegalArgumentException("Expected: "); + if (args.length != 9) { + throw new IllegalArgumentException("Expected: "); } return new ProbeConfiguration( new File(args[0]), @@ -154,7 +154,8 @@ public final class GenerationProbe { Integer.parseInt(args[4]), Integer.parseInt(args[5]), Boolean.parseBoolean(args[6]), - Boolean.parseBoolean(args[7])); + Boolean.parseBoolean(args[7]), + Long.parseLong(args[8])); } } @@ -277,7 +278,9 @@ public final class GenerationProbe { throw new IllegalArgumentException("Pack folder not found: " + configuration.packSource().getAbsolutePath()); } - File workRoot = Files.createTempDirectory("iris-genprobe-").toFile(); + Path temporaryRoot = Path.of("build", "generation-probe").toAbsolutePath(); + Files.createDirectories(temporaryRoot); + File workRoot = Files.createTempDirectory(temporaryRoot, "iris-genprobe-").toFile(); IrisData data = null; Engine engine = null; Throwable executionFailure = null; @@ -288,6 +291,7 @@ public final class GenerationProbe { System.out.println("[genprobe] pack: " + configuration.packSource().getAbsolutePath()); System.out.println("[genprobe] dimension: " + configuration.dimensionKey()); + System.out.println("[genprobe] seed: " + configuration.seed()); System.out.println("[genprobe] warmup chunks: " + configuration.warmupChunks()); System.out.println("[genprobe] measured chunks: " + configuration.measuredChunks()); System.out.println("[genprobe] center chunk: " + configuration.centerChunkX() + "," + configuration.centerChunkZ()); @@ -304,7 +308,7 @@ public final class GenerationProbe { IrisWorld world = IrisWorld.builder() .platformIdentity("iris:probe") .name("probe") - .seed(SEED) + .seed(configuration.seed()) .worldFolder(new File(workRoot, "world")) .minHeight(dimension.getMinHeight()) .maxHeight(dimension.getMaxHeight()) @@ -323,7 +327,9 @@ public final class GenerationProbe { + " seed=" + engine.getSeedManager().getSeed() + " minY=" + engine.getMinHeight() + " maxY=" + engine.getMaxHeight() + " timeMs=" + String.format(Locale.ROOT, "%.3f", nanosToMillis(engineReadyNanos))); + long nativeSamplesBefore = FractalBillowSimplexNoise.nativeSampleCount(); GenerationResult generation = generate(engine, configuration); + verifyBackend(nativeSamplesBefore); String status = generation.failedChunks() == 0 ? "PASS" : "FAIL"; printGenerationFailures(generation); return new ProbeResult( @@ -354,6 +360,20 @@ public final class GenerationProbe { } } + private static void verifyBackend(long nativeSamplesBefore) { + String backend = FractalBillowSimplexNoise.nativeBackendStatus(); + long nativeSamples = FractalBillowSimplexNoise.nativeSampleCount() - nativeSamplesBefore; + System.out.println("IRIS_GENPROBE_BACKEND status=" + backend + " native_samples=" + nativeSamples + + " diagnostics=" + Boolean.getBoolean("volmlib.noise.nativeBillowDiagnostics")); + String expected = System.getProperty("iris.probe.expectedBackend", ""); + if (!expected.isEmpty() && !expected.equals(backend)) { + throw new IllegalStateException("Expected backend " + expected + ", received " + backend); + } + if (Boolean.getBoolean("iris.probe.requireNativeSamples") && nativeSamples <= 0L) { + throw new IllegalStateException("Real chunk generation did not report native Billow samples."); + } + } + private static void configureProbeRuntime(File workRoot) { REPORTED.clear(); StubPlatform.bindGenerationStateHandlers(); diff --git a/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java index 4b7e37338..6334befe5 100644 --- a/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java +++ b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java @@ -24,7 +24,8 @@ public final class GenerationProbeTest { "2048", "-2048", "true", - "true" + "true", + "-1" }); assertEquals(new File("/tmp/pack"), configuration.packSource()); @@ -35,6 +36,7 @@ public final class GenerationProbeTest { assertEquals(-2048, configuration.centerChunkZ()); assertTrue(configuration.multicore()); assertTrue(configuration.studio()); + assertEquals(-1L, configuration.seed()); } @Test @@ -42,11 +44,25 @@ public final class GenerationProbeTest { assertThrows(IllegalArgumentException.class, () -> GenerationProbe.ProbeConfiguration.parse(new String[]{"/tmp/pack"})); assertThrows(IllegalArgumentException.class, - () -> new GenerationProbe.ProbeConfiguration(new File("/tmp/pack"), " ", 1, 1, 0, 0, false, false)); + () -> new GenerationProbe.ProbeConfiguration(new File("/tmp/pack"), " ", 1, 1, 0, 0, false, false, 1337L)); assertThrows(IllegalArgumentException.class, - () -> new GenerationProbe.ProbeConfiguration(new File("/tmp/pack"), "overworld", 0, 1, 0, 0, false, false)); + () -> new GenerationProbe.ProbeConfiguration(new File("/tmp/pack"), "overworld", 0, 1, 0, 0, false, false, 1337L)); assertThrows(IllegalArgumentException.class, - () -> new GenerationProbe.ProbeConfiguration(new File("/tmp/pack"), "overworld", 1, 0, 0, 0, false, false)); + () -> new GenerationProbe.ProbeConfiguration(new File("/tmp/pack"), "overworld", 1, 0, 0, 0, false, false, 1337L)); + } + + @Test + public void parsesSignedBorderCoordinatesAndFullWidthSeeds() { + GenerationProbe.ProbeConfiguration configuration = GenerationProbe.ProbeConfiguration.parse(new String[]{ + "/tmp/pack", "overworld", "32", "96", "1874000", "-1874000", "false", "false", + Long.toString(Long.MIN_VALUE) + }); + assertEquals(Long.MIN_VALUE, configuration.seed()); + assertEquals(-1874000, configuration.centerChunkZ()); + assertThrows(NumberFormatException.class, + () -> GenerationProbe.ProbeConfiguration.parse(new String[]{ + "/tmp/pack", "overworld", "32", "96", "0", "0", "false", "false", "invalid" + })); } @Test diff --git a/tools/compare-native-generation.py b/tools/compare-native-generation.py new file mode 100644 index 000000000..30e8e4481 --- /dev/null +++ b/tools/compare-native-generation.py @@ -0,0 +1,99 @@ +import argparse +import fcntl +import json +import os +from pathlib import Path +import statistics +import subprocess +import time + + +def fields(line): + return dict(part.split('=', 1) for part in line.split()[1:]) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--pack', required=True) + parser.add_argument('--dimension', required=True) + parser.add_argument('--volmlib', required=True) + parser.add_argument('--seed', type=int, default=1337) + parser.add_argument('--center-x', type=int, default=2048) + parser.add_argument('--center-z', type=int, default=2048) + parser.add_argument('--warmup', type=int, default=64) + parser.add_argument('--measured', type=int, default=192) + parser.add_argument('--pairs', type=int, default=2) + parser.add_argument('--diagnostics', action='store_true') + parser.add_argument('--output', required=True) + args = parser.parse_args() + if args.pairs < 1: + parser.error('--pairs must be positive') + root = Path(__file__).resolve().parents[1] + output = Path(args.output).resolve() + if not any(output.is_relative_to(root / allowed) for allowed in ('.perf', 'build')): + parser.error('--output must be inside this checkout\'s .perf or build directory') + output.mkdir(parents=True, exist_ok=False) + environment = os.environ.copy() + environment.pop('GIT_CONFIG_COUNT', None) + for variable in ('JAVA_TOOL_OPTIONS', '_JAVA_OPTIONS', 'JDK_JAVA_OPTIONS'): + environment.pop(variable, None) + results = [] + expected_hashes = None + with open('/tmp/iris-acceleration-integration-benchmark.lock', 'a') as lock: + print('Waiting for generation benchmark lock', flush=True) + fcntl.flock(lock, fcntl.LOCK_EX) + for pair in range(args.pairs): + for native in ([False, True] if pair % 2 == 0 else [True, False]): + backend = 'native' if native else 'java' + command = [str(root / 'gradlew'), ':probe:genProbe', '--no-daemon', + '-PincludeNativeBillow=true', f'-PlocalVolmLibDirectory={args.volmlib}', + f'-PprobePack={Path(args.pack).resolve()}', f'-PprobeDimension={args.dimension}', + f'-PprobeSeed={args.seed}', f'-PprobeCenterChunkX={args.center_x}', + f'-PprobeCenterChunkZ={args.center_z}', f'-PprobeWarmupChunks={args.warmup}', + f'-PprobeMeasuredChunks={args.measured}', '-PprobeMulticore=false', + '-PprobeStudio=false', f'-PprobeNativeBillow={str(native).lower()}', + '-PprobeNativeAccess=true', + f'-PprobeNativeDiagnostics={str(args.diagnostics).lower()}', + f'-PprobeRequireNativeSamples={str(native and args.diagnostics).lower()}', + f'-PprobeExpectedBackend={"native" if native else "java-disabled"}'] + label = f'{pair + 1}-{backend}' + (output / f'{label}.command.json').write_text(json.dumps(command, indent=2) + '\n') + print(f'Running {label}', flush=True) + started = time.monotonic() + with (output / f'{label}.log').open('w') as log: + completed = subprocess.run(command, cwd=root, env=environment, stdout=log, + stderr=subprocess.STDOUT, check=False) + text = (output / f'{label}.log').read_text() + lines = text.splitlines() + hashes = [line for line in lines if line.startswith('GENPROBE_CHUNK_HASH ')] + result_lines = [line for line in lines if line.startswith('IRIS_GENPROBE_RESULT ')] + backend_lines = [line for line in lines if line.startswith('IRIS_GENPROBE_BACKEND ')] + if completed.returncode != 0 or len(result_lines) != 1 or len(backend_lines) != 1: + raise RuntimeError(f'{label} failed; see {output / (label + ".log")}') + result = fields(result_lines[0]) + status = fields(backend_lines[0]) + if result['status'] != 'PASS' or len(hashes) != args.warmup + args.measured: + raise RuntimeError(f'{label} did not generate every chunk successfully') + expected_backend = 'native' if native else 'java-disabled' + if status['status'] != expected_backend: + raise RuntimeError(f'{label} backend mismatch: {status}') + if expected_hashes is None: + expected_hashes = hashes + elif hashes != expected_hashes: + raise RuntimeError(f'{label} chunk block/biome hashes differ') + results.append({'pair': pair + 1, 'backend': backend, 'result': result, + 'backend_status': status, 'wall_seconds': time.monotonic() - started}) + (output / 'results.json').write_text(json.dumps(results, indent=2) + '\n') + rates = {backend: statistics.median(float(run['result']['measured_cps']) for run in results + if run['backend'] == backend) for backend in ('java', 'native')} + summary = {'median_chunks_per_second': rates, + 'native_throughput_change_percent': (rates['native'] / rates['java'] - 1) * 100, + 'all_chunk_hashes_equal': True, 'diagnostics_enabled': args.diagnostics, + 'run_count': len(results), 'seed': args.seed, + 'unique_chunks': args.warmup + args.measured} + (output / 'summary.json').write_text(json.dumps(summary, indent=2) + '\n') + print(json.dumps(summary, indent=2)) + + +if __name__ == '__main__': + main()