entity bvh with exact old tie order and allocation-free entity/element intersectors, entity benchmark harness and perf test scenes
This commit is contained in:
1 parent
2d56a1910c
commit
b5baffebbd
6 files changed
+910
-161
No files matched your search
@@ -0,0 +1,195 @@
|
||||
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<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));
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/** 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<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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user