Files
PixelPics/tools/EntityBenchmark.java
T

201 lines
11 KiB
Java

import eu.mhsl.minecraft.pixelpics.render.entity.*;
import eu.mhsl.minecraft.pixelpics.render.render.RenderJob;
import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot;
import org.bukkit.ChunkSnapshot;
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;
/**
* Reproducible render benchmark (no server): farm scenes with hundreds of mobs / dropped items on a grass floor
* plus a "normal" scene (few mobs, translucent + additive layers). Renders every scene {@code -Druns} times (after
* {@code -Dwarmup} runs), prints median/min 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 (default all):
* farm_mobs, farm_empty (same view, no entities), farm_mobs_close, farm_items, village, normal.
* {@code -Djava.util.concurrent.ForkJoinPool.common.parallelism=N} pins the thread count. Build: see {@link Harness}.
*/
public class EntityBenchmark {
static final int W = 320, H = 240;
record Scene(String name, Vector cam, Vector look, List<EntityState> mobs, List<ObjectState> objects) {}
public static void main(String[] args) throws Exception {
Harness h = new Harness("bench");
WorldSnapshot world = farmWorld();
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<String> only = new HashSet<>(Arrays.asList(args));
for (Scene sc : scenes()) {
if (!only.isEmpty() && !only.contains(sc.name)) continue;
RenderJob job = Harness.job(world, sc.cam, sc.look, W, H, sc.mobs, List.of(), List.of(), sc.objects);
BufferedImage img = null;
for (int i = 0; i < warmup; i++) img = h.renderer.execute(job);
long[] times = new long[runs];
for (int i = 0; i < runs; i++) {
long t0 = System.nanoTime();
img = h.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<Scene> scenes() throws ReflectiveOperationException {
List<Scene> out = new ArrayList<>();
// Mob farm: 400 animals in a 20x20 pen (1.4 block spacing), seen from an elevated corner.
List<EntityState> 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<ObjectState> 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(), Harness.block(blocks[b], blockState[b]), null, false);
} else {
spec = Harness.item(flat[r.nextInt(flat.length)]);
}
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));
// Village: 300 mobs with per-mob texture compositing (villager type/profession/level overlays, dyed
// sheep wool, dyed leather armor, tinted wolf collars) — exercises the tinted-texture cache.
List<EntityState> village = new ArrayList<>();
String[] vTypes = {"plains", "desert", "jungle", "savanna", "snow", "swamp", "taiga"};
String[] profs = {"armorer", "butcher", "cartographer", "cleric", "farmer", "fisherman", "fletcher",
"leatherworker", "librarian", "mason", "shepherd", "toolsmith", "weaponsmith", "nitwit", "none"};
Random rv = new Random(7);
for (int i = 0; i < 15; i++) {
for (int j = 0; j < 20; j++) {
double x = i * 1.4 + rv.nextDouble() * 0.3, z = j * 1.4 + rv.nextDouble() * 0.3;
float yaw = rv.nextFloat() * 360;
int dye = DYES[rv.nextInt(DYES.length)];
EntityState m = switch (rv.nextInt(4)) {
case 0 -> villager(x, z, yaw, vTypes[rv.nextInt(vTypes.length)], profs[rv.nextInt(profs.length)],
1 + rv.nextInt(5), rv.nextInt(6) == 0);
case 1 -> with(mob("sheep", x, 1, z, yaw, rv.nextInt(6) == 0), dye, null, 0);
case 2 -> {
EntityState.EquipPiece le = new EntityState.EquipPiece("leather", dye, null, null, false);
yield with(mob("zombie", x, 1, z, yaw, false), 0, new EntityState.Equipment(le, le, le, le), 0);
}
default -> with(mob("wolf", x, 1, z, yaw, rv.nextInt(6) == 0), 0, null, dye);
};
village.add(m);
}
}
out.add(new Scene("village", new Vector(-5, 10, -5), new Vector(10, 1, 13), village, List.of()));
// 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<EntityState> 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));
few.add(Harness.copy(mob("creeper", 7, 1, 3.5, 250, false), Map.of("powered", true)));
few.add(mob("chicken", 3.5, 1, 1.5, 90, false));
List<ObjectState> objs = new ArrayList<>();
objs.add(ObjectState.item(Affine.translation(4.5, 1.2, 2).mul(Affine.scale(0.5)), Harness.item("apple"), "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, null, null, null, null);
}
/** Vanilla DyeColor texture colours (white … black). */
static final int[] DYES = {0xFFF9FFFE, 0xFFF9801D, 0xFFC74EBD, 0xFF3AB3DA, 0xFFFED83D, 0xFF80C71F, 0xFFF38BAA,
0xFF474F52, 0xFF9D9D97, 0xFF169C9C, 0xFF8932B8, 0xFF3C44AA, 0xFF835432, 0xFF5E7C16, 0xFFB02E26, 0xFF1D1D21};
static EntityState villager(double x, double z, float yaw, String type, String prof, int level, boolean baby)
throws ReflectiveOperationException {
return Harness.copy(mob("villager", x, 1, z, yaw, baby), Map.of("width", 0.6, "height", 1.95, "variant", type,
"profession", prof, "villagerLevel", level));
}
/** {@code s} with wool tint, humanoid armor and collar dye replaced. */
static EntityState with(EntityState s, int tint, EntityState.Equipment eq, int collar)
throws ReflectiveOperationException {
Map<String, Object> o = new HashMap<>();
o.put("tint", tint);
o.put("equipment", eq);
o.put("collar", collar);
return Harness.copy(s, o);
}
/** 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";
case "wolf" -> "pale";
default -> null;
};
}
/**
* 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 = Harness.block(Material.AIR, null);
BlockData grass = Harness.block(Material.GRASS_BLOCK, "snowy=false");
Map<Long, ChunkSnapshot> chunks = new HashMap<>();
for (int cx = -3; cx < 3; cx++) {
for (int cz = -3; cz < 3; cz++) {
ChunkSnapshot cs = (ChunkSnapshot) Proxy.newProxyInstance(EntityBenchmark.class.getClassLoader(),
new Class<?>[]{ChunkSnapshot.class}, (p, m, a) -> switch (m.getName()) {
case "getBlockData" -> (int) a[1] == 0 ? grass : air;
case "equals" -> p == a[0];
case "hashCode" -> System.identityHashCode(p);
case "toString" -> "chunk";
default -> Harness.defaultValue(m.getReturnType());
});
chunks.put(WorldSnapshot.chunkKey(cx, cz), cs);
}
}
return new WorldSnapshot(chunks, 0, 8, air);
}
}