import eu.mhsl.minecraft.pixelpics.assets.*; import eu.mhsl.minecraft.pixelpics.render.entity.*; import eu.mhsl.minecraft.pixelpics.render.render.*; import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext; import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot; import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; import org.bukkit.ChunkSnapshot; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.data.BlockData; import org.bukkit.util.Vector; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.lang.reflect.Proxy; import java.util.*; import java.util.List; import java.util.logging.Logger; /** * Reproducible render benchmark (no server): farm scenes with hundreds of mobs / dropped items on a block * floor plus a "normal" scene (few mobs, translucent + additive layers). Renders every scene {@code -Druns} * times (after {@code -Dwarmup} runs), prints the median wall time and writes one PNG per scene to * {@code -Dout} (default ~/Dokumente/PixelPics-Entity-Renders/bench) for before/after pixel diffs. * Args: scene names to run (default all): farm_mobs, farm_empty (same view, no entities), farm_mobs_close, * farm_items, normal. {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count. */ public class EntityBenchmark { static final String ROOT = "/home/elias/Dokumente/mcTestServer/plugins/PixelPics"; static final int W = 320, H = 240, SSAA = 3; record Scene(String name, Vector cam, Vector look, List mobs, List objects) {} public static void main(String[] args) throws Exception { Logger log = Logger.getLogger("bench"); ResourcePack pack = ResourcePackLoader.load(new File(ROOT, "resourcepack"), log).orElseThrow(); AssetReader reader = new AssetReader(pack); TextureCache textures = new TextureCache(pack); BlockModelRegistry registry = new BlockModelRegistry(reader, textures); BiomeTintProvider tint = new BiomeTintProvider(textures); var geo = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader(); geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log); geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log); var font = eu.mhsl.minecraft.pixelpics.assets.font.FontLoader.load(pack, textures, log); SkinCache skins = new SkinCache(); var baker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker(geo, textures, skins); var beBaker = new eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker(geo, textures, skins, font); DefaultScreenRenderer renderer = new DefaultScreenRenderer(registry, tint, textures, baker, beBaker, log); WorldSnapshot world = farmWorld(); SkyContext sky = new SkyContext(6000, 0, 6000); int runs = Integer.getInteger("runs", 5), warmup = Integer.getInteger("warmup", 2); File outDir = new File(System.getProperty("out", "/home/elias/Dokumente/PixelPics-Entity-Renders/bench")); outDir.mkdirs(); Set only = new HashSet<>(Arrays.asList(args)); for (Scene sc : scenes()) { if (!only.isEmpty() && !only.contains(sc.name)) continue; Location loc = new Location(null, sc.cam.getX(), sc.cam.getY(), sc.cam.getZ()); loc.setDirection(sc.look.clone().subtract(sc.cam)); List rayMap = EntityTestRender.buildRayMap(loc, W * SSAA, H * SSAA); RenderJob job = new RenderJob(world, rayMap, sc.cam, W, H, sky, sc.mobs, List.of(), List.of(), sc.objects); BufferedImage img = null; for (int i = 0; i < warmup; i++) img = renderer.execute(job); long[] times = new long[runs]; for (int i = 0; i < runs; i++) { long t0 = System.nanoTime(); img = renderer.execute(job); times[i] = System.nanoTime() - t0; } Arrays.sort(times); ImageIO.write(img, "png", new File(outDir, sc.name + ".png")); System.out.printf("BENCH %-16s mobs=%4d objects=%4d median %7.1f ms min %7.1f ms%n", sc.name, sc.mobs.size(), sc.objects.size(), times[runs / 2] / 1e6, times[0] / 1e6); } } static List scenes() { List out = new ArrayList<>(); // Mob farm: 400 animals in a 20x20 pen (1.4 block spacing), seen from an elevated corner. List farm = new ArrayList<>(); Random r = new Random(42); String[] types = {"cow", "sheep", "chicken", "pig"}; for (int i = 0; i < 20; i++) { for (int j = 0; j < 20; j++) { String type = types[r.nextInt(types.length)]; farm.add(mob(type, i * 1.4 + r.nextDouble() * 0.4, 1, j * 1.4 + r.nextDouble() * 0.4, r.nextFloat() * 360, r.nextInt(5) == 0)); } } out.add(new Scene("farm_mobs", new Vector(-6, 14, -6), new Vector(14, 1, 14), farm, List.of())); // Same view without entities: farm_mobs minus this = the entity share of the render time. out.add(new Scene("farm_empty", new Vector(-6, 14, -6), new Vector(14, 1, 14), List.of(), List.of())); out.add(new Scene("farm_mobs_close", new Vector(3, 4.5, -3), new Vector(10, 1, 8), farm, List.of())); // Item farm: 500 dropped items (flat sprites + block items) piled on a 10x10 collection area. List items = new ArrayList<>(); String[] flat = {"wheat", "carrot", "egg", "bone", "rotten_flesh", "string", "gunpowder", "iron_ingot"}; Material[] blocks = {Material.COBBLESTONE, Material.OAK_LOG, Material.PUMPKIN}; String[] blockState = {null, "axis=y", null}; for (int i = 0; i < 500; i++) { double x = 2 + r.nextDouble() * 10, z = 2 + r.nextDouble() * 10, y = 1.1 + r.nextDouble() * 0.3; Affine a = Affine.translation(x, y, z).mul(Affine.rotY(r.nextDouble() * Math.PI * 2)).mul(Affine.scale(0.5)); ItemSpec spec; if (r.nextInt(4) == 0) { int b = r.nextInt(blocks.length); spec = new ItemSpec(blocks[b].getKey().getKey(), ObjectTestRender.block(blocks[b], blockState[b]), null, false); } else { spec = new ItemSpec(flat[r.nextInt(flat.length)], null, null, false); } items.add(ObjectState.item(a, spec, "ground", true)); } out.add(new Scene("farm_items", new Vector(0, 7, -1), new Vector(7, 1, 7), List.of(), items)); // Normal scene: a handful of mobs incl. translucent (slime) and additive (charged creeper) layers, // a text display with the default translucent background and two items. List few = new ArrayList<>(); few.add(mob("cow", 3, 1, 3, 30, false)); few.add(mob("sheep", 5, 1, 4, 200, false)); few.add(mob("villager", 4, 1, 6, 160, false)); few.add(mob("zombie", 6.5, 1, 6, 220, false)); few.add(mob("slime", 2, 1, 5.5, 120, false)); EntityState creeper = mob("creeper", 7, 1, 3.5, 250, false); few.add(withPowered(creeper)); few.add(mob("chicken", 3.5, 1, 1.5, 90, false)); List objs = new ArrayList<>(); objs.add(ObjectState.item(Affine.translation(4.5, 1.2, 2).mul(Affine.scale(0.5)), new ItemSpec("apple", null, null, false), "ground", true)); objs.add(ObjectState.text(Affine.translation(4.5, 3.2, 4).mul(Affine.rotY(Math.PI)).mul(Affine.scale(0.025)), List.of("PixelPics bench"), 200, 0xFFFFFFFF, 0x40000000, true)); out.add(new Scene("normal", new Vector(0, 3.5, -2.5), new Vector(4.5, 1.5, 4), few, objs)); return out; } static EntityState mob(String type, double x, double y, double z, float yaw, boolean baby) { int tint = type.equals("sheep") ? 0xFFF9FFFE : 0; return new EntityState(type, x, y, z, yaw, baby, 0.8, 1.0, false, null, false, variant(type), tint, 1.0, type.equals("villager") ? "farmer" : null, type.equals("villager") ? 2 : 0, null, false, false, false, null, null, false, 0, 0, 0, false, null, null, null, 0, false, null); } /** The variant the game always supplies (cow/pig/chicken have no texture without one). */ static String variant(String type) { return switch (type) { case "villager" -> "plains"; case "cow", "pig", "chicken" -> "temperate"; default -> null; }; } static EntityState withPowered(EntityState s) { return new EntityState(s.typeKey(), s.x(), s.y(), s.z(), s.bodyYaw(), s.baby(), s.width(), s.height(), s.player(), s.skinUrl(), s.slim(), s.variant(), s.tint(), s.sizeScale(), s.profession(), s.villagerLevel(), s.markings(), s.saddle(), s.chest(), s.arms(), s.bodyEquip(), s.equipment(), s.invisible(), s.pitch(), s.bodyDye(), s.tint2(), s.passenger(), s.rightHand(), s.leftHand(), s.headItem(), s.collar(), true, s.mouthItem()); } /** * Grass floor at y=0 over [-48, 48)². Nothing stacked on it: ambient occlusion asks Material.isOccluding() * for non-air neighbours, which needs a server registry (not available standalone). */ static WorldSnapshot farmWorld() { BlockData air = ObjectTestRender.block(Material.AIR, null); BlockData grass = ObjectTestRender.block(Material.GRASS_BLOCK, "snowy=false"); Map chunks = new HashMap<>(); for (int cx = -3; cx < 3; cx++) { for (int cz = -3; cz < 3; cz++) { final int bx0 = cx * 16, bz0 = cz * 16; ChunkSnapshot cs = (ChunkSnapshot) Proxy.newProxyInstance(EntityBenchmark.class.getClassLoader(), new Class[]{ChunkSnapshot.class}, (p, m, a) -> { switch (m.getName()) { case "getBlockData": { int y = (int) a[1]; return y == 0 ? grass : air; } case "getBiome": return null; case "equals": return p == a[0]; case "hashCode": return System.identityHashCode(p); case "toString": return "chunk"; } Class rt = m.getReturnType(); if (rt == boolean.class) return false; if (rt.isPrimitive()) return 0; return null; }); chunks.put(WorldSnapshot.chunkKey(cx, cz), cs); } } return new WorldSnapshot(chunks, 0, 8, air); } }