Files
PixelPics/tools/EntityBenchmark.java
T

244 lines
14 KiB
Java

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, village, 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<EntityState> mobs, List<ObjectState> 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<String> 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<Vector> 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<Scene> scenes() {
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(), 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));
// 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));
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<ObjectState> 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);
}
/** 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) {
return new EntityState("villager", x, 1, z, yaw, baby, 0.6, 1.95,
false, null, false, type, 0, 1.0, prof, level,
null, false, false, false, null, null,
false, 0, 0, 0, false, null, null, null, 0, false, null);
}
/** {@code s} with wool tint, humanoid armor and collar dye replaced. */
static EntityState with(EntityState s, int tint, EntityState.Equipment eq, int collar) {
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(), tint, s.sizeScale(), s.profession(), s.villagerLevel(),
s.markings(), s.saddle(), s.chest(), s.arms(), s.bodyEquip(), eq, s.invisible(), s.pitch(),
s.bodyDye(), s.tint2(), s.passenger(), s.rightHand(), s.leftHand(), s.headItem(), collar, s.powered(),
s.mouthItem());
}
/** 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;
};
}
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<Long, ChunkSnapshot> 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);
}
}