package com.nilunder.bdx.utils; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import com.badlogic.gdx.utils.TimeUtils; import com.badlogic.gdx.graphics.profiling.GLErrorListener; import com.nilunder.bdx.Bdx; import com.nilunder.bdx.Scene; import com.nilunder.bdx.GameObject; import com.nilunder.bdx.Text; import com.nilunder.bdx.gl.Viewport; import javax.vecmath.Vector2f; import javax.vecmath.Vector3f; public class Profiler{ public class Gl extends com.badlogic.gdx.graphics.profiling.GLProfiler{ private HashMap<String, Text> texts; private HashMap<String, Integer> stats; private HashMap<String, Integer> fields; private String[] names; { names = new String[]{ "calls", "drawCalls", "shaderSwitches", "textureBindings", "vertexCount" }; stats = new HashMap<String, Integer>(); fields = new HashMap<String, Integer>(); for (String name : names){ stats.put(name, 0); fields.put(name, 0); } } protected void initTexts(){ texts = new HashMap<String, Text>(); Vector3f position = new Vector3f(SPACING, verticalOffset(0.5f), 0); for (String name : names){ position.y = verticalOffset(1); texts.put(name, (Text)add("__PText", position)); } position.y = verticalOffset(1); texts.put("triangleCount", (Text)add("__PText", position)); } public int calls(){ return stats.get("calls"); } public int drawCalls(){ return stats.get("drawCalls"); } public int shaderSwitches(){ return stats.get("shaderSwitches"); } public int textureBindings(){ return stats.get("textureBindings"); } public int vertexCount(){ return stats.get("vertexCount"); } public int triangleCount(){ return vertexCount() / 3; } public void print(){ StringBuffer buffer = new StringBuffer("\n"); buffer.append(formatForGl("gl calls", calls()) + "\n"); buffer.append(formatForGl("gl draw calls", drawCalls()) + "\n"); buffer.append(formatForGl("gl shader switches", shaderSwitches()) + "\n"); buffer.append(formatForGl("gl texture bindings", textureBindings()) + "\n"); buffer.append(formatForGl("gl vertex count", vertexCount()) + "\n"); buffer.append(formatForGl("gl triangle count", triangleCount()) + "\n"); System.out.println(buffer.toString()); } protected void updateTexts(){ texts.get("calls").text(formatForGl("gl calls", calls())); texts.get("drawCalls").text(formatForGl("gl draw calls", drawCalls())); texts.get("shaderSwitches").text(formatForGl("gl shader switches", shaderSwitches())); texts.get("textureBindings").text(formatForGl("gl texture bindings", textureBindings())); texts.get("vertexCount").text(formatForGl("gl vertex count", vertexCount())); texts.get("triangleCount").text(formatForGl("gl triangle count", triangleCount())); } protected void updateStats(){ stats.put("calls", calls - fields.get("calls")); stats.put("drawCalls", drawCalls - fields.get("drawCalls")); stats.put("shaderSwitches", shaderSwitches - fields.get("shaderSwitches")); stats.put("textureBindings", textureBindings - fields.get("textureBindings")); stats.put("vertexCount", (int)vertexCount.total - fields.get("vertexCount")); } public void updateFields(){ fields.put("calls", calls); fields.put("drawCalls", drawCalls); fields.put("shaderSwitches", shaderSwitches); fields.put("textureBindings", textureBindings); fields.put("vertexCount", (int)vertexCount.total); } } public class Props extends HashMap<String, String>{ protected HashMap<String, Text> texts = new HashMap<String, Text>(); protected boolean updateHorizontalOffset(){ int maxLen = 0; int len; for (Text text : texts.values()){ len = text.text().length(); if (maxLen < len){ maxLen = len; } } float maxOffset = maxLen * FONT_SIZE + 1; if (maxOffset == horizontalOffset){ return false; }else if (maxOffset <= HORIZONTAL_OFFSET_DEFAULT){ if (horizontalOffset == HORIZONTAL_OFFSET_DEFAULT){ return false; }else{ horizontalOffset = HORIZONTAL_OFFSET_DEFAULT; } }else{ horizontalOffset = maxOffset; } return true; } protected void initTexts(){ if (isEmpty()){ return; } Vector3f position = new Vector3f(SPACING, verticalOffset(0.5f), 0); Text text; String key; for (Map.Entry<String, String> e : entrySet()){ position.y = verticalOffset(1); text = (Text)add("__PText", position); key = e.getKey(); text.text(formatForProps(key, e.getValue())); texts.put(key, text); } updateHorizontalOffset(); } @Override public void putAll(Map<? extends String, ? extends String> map){ super.putAll(map); for (Map.Entry<String, String> e : entrySet()){ String key = e.getKey(); if (!texts.containsKey(key)){ reinitialize(); return; } texts.get(key).text(formatForProps(key, e.getValue())); } if (updateHorizontalOffset()){ scaleBackground(); } } @Override public String put(String key, String value) { super.put(key, value); if (texts.containsKey(key)){ texts.get(key).text(formatForProps(key, value)); if (updateHorizontalOffset()){ scaleBackground(); } }else{ reinitialize(); } return value; } public String put(String key, int value) { return put(key, String.valueOf(value)); } public String put(String key, float value) { return put(key, String.valueOf(value)); } public String put(String key, double value) { return put(key, String.valueOf(value)); } public String put(String key, char value) { return put(key, String.valueOf(value)); } public String put(String key, long value) { return put(key, String.valueOf(value)); } public String put(String key, Object value) { return put(key, String.valueOf(value)); } @Override public String remove(Object key){ String value = super.remove(key); reinitialize(); return value; } @Override public void clear(){ super.clear(); reinitialize(); } } private final Color BG_COLOR = new Color(0.125f, 0.125f, 0.125f, 0.75f); private final float SPACING = 0.6f; private final float BAR_HEIGHT = 0.4f; private final float BAR_WIDTH = SPACING * 4; private final float BAR_POSITION = SPACING * 18; private final float HORIZONTAL_OFFSET_DEFAULT = BAR_POSITION + SPACING + BAR_WIDTH; private final float FONT_SIZE = 0.315f; private final String EXC_MSG = "User created subsystem names should not start with: \"__\""; private long totalStartTime; private long totalDeltaTime; private long lastStopTime; public HashMap<String, Long> deltaTimes; private HashMap<String, Long> startTimes; private ArrayList<Long> tickTimes; private float counter; private GameObject display; private GameObject background; private Text tickInfo; private HashMap<String, Text> texts; private HashMap<String, GameObject> bars; private boolean visible; private boolean tickInfoVisible; private boolean subsystemsVisible; private boolean glVisible; private boolean propsVisible; private float verticalOffset; private float horizontalOffset; public HashMap<String, Long> nanos; public HashMap<String, Float> percents; public Scene scene; public int frequency; public float avgTickRate; public float avgTickTime; public Gl gl; public Props props; { totalStartTime = TimeUtils.nanoTime(); deltaTimes = new HashMap<String, Long>(); startTimes = new HashMap<String, Long>(); nanos = new HashMap<String, Long>(); percents = new HashMap<String, Float>(); tickTimes = new ArrayList<Long>(); for (int i=0; i < Bdx.TICK_RATE; i++){ tickTimes.add((long) Bdx.TICK_TIME); } frequency = Bdx.TICK_RATE; counter = 1; avgTickRate = Bdx.TICK_RATE; avgTickTime = Bdx.TICK_TIME; tickInfoVisible = true; propsVisible = true; subsystemsVisible = true; scene = null; gl = new Gl(); gl.listener = GLErrorListener.THROWING_LISTENER; props = new Props(); } private float verticalOffset(float factor){ verticalOffset -= SPACING * factor; return verticalOffset; } private void addTextAndBar(String name){ Vector3f position = new Vector3f(SPACING, verticalOffset, 0); texts.put(name, (Text)add("__PText", position)); position.x = BAR_POSITION; bars.put(name, add("__PBar", position)); } private void scaleBackground(){ background.scale(horizontalOffset, SPACING - verticalOffset, 1); } private void initTickInfo(){ tickInfo = (Text)add("__PText", new Vector3f(SPACING, verticalOffset(1.5f), 0)); } private void initSubsystems(){ String[] names = { "__render", "__logic", "__scene", "__physics", "__outside", "__gpu wait" }; texts = new HashMap<String, Text>(); bars = new HashMap<String, GameObject>(); verticalOffset(0.5f); for (String name : names){ verticalOffset(1); addTextAndBar(name); } } private void initialize(){ horizontalOffset = HORIZONTAL_OFFSET_DEFAULT; verticalOffset = 0; if (tickInfoVisible){ initTickInfo(); } if (propsVisible){ props.initTexts(); } if (glVisible){ gl.enable(); gl.initTexts(); } if (subsystemsVisible){ initSubsystems(); } scaleBackground(); } public void init(){ visible = true; scene = new Scene("__Profiler"); scene.init(); display = scene.add("__PDisplay"); background = display.children.get("__PBackground"); background.mesh().materials.color(BG_COLOR); scene.viewport.type(Viewport.Type.SCREEN); updateViewport(); initialize(); } private void reinitialize(){ if (!visible){ return; } tickInfo.end(); for (Text text : texts.values()){ text.end(); } texts.clear(); for (GameObject bar : bars.values()){ bar.end(); } bars.clear(); for (Text text : props.texts.values()){ text.end(); } props.texts.clear(); initialize(); } public boolean visible(){ return visible; } public void visible(boolean visible) { if (scene == null && visible) init(); this.visible = visible; } public boolean tickInfoVisible(){ return tickInfoVisible; } public void tickInfoVisible(boolean visible){ if (!this.visible){ return; } if (tickInfoVisible == visible){ return; } tickInfoVisible = visible; reinitialize(); } public boolean subsystemsVisible(){ return subsystemsVisible; } public void subsystemsVisible(boolean visible){ if (!this.visible){ return; } if (subsystemsVisible == visible){ return; } subsystemsVisible = visible; reinitialize(); } public boolean glVisible(){ return glVisible; } public void glVisible(boolean visible){ if (!this.visible){ return; } if (glVisible == visible){ return; } glVisible = visible; reinitialize(); } public boolean propsVisible(){ return propsVisible; } public void propsVisible(boolean visible){ if (!this.visible){ return; } if (propsVisible == visible){ return; } propsVisible = visible; reinitialize(); } public void start(String name){ startTimes.put(name, TimeUtils.nanoTime()); } public float stop(String name){ long stopTime = TimeUtils.nanoTime(); long startTime; if (startTimes.containsKey(name)){ startTime = startTimes.get(name); startTimes.remove(name); }else{ startTime = lastStopTime; } long deltaTime = stopTime - startTime; if (subsystemsVisible){ long storedDeltaTime = (long) (deltaTime * (float) frequency / Bdx.TICK_RATE); if (deltaTimes.containsKey(name)){ storedDeltaTime += deltaTimes.get(name); } deltaTimes.put(name, storedDeltaTime); lastStopTime = stopTime; } return deltaTime * 0.000001f; } public void remove(String name){ if (name.startsWith("__")){ throw new RuntimeException(EXC_MSG); } deltaTimes.remove(name); startTimes.remove(name); nanos.remove(name); percents.remove(name); if (visible && subsystemsVisible){ texts.get(name).end(); texts.remove(name); bars.get(name).end(); bars.remove(name); verticalOffset(-1); scaleBackground(); } } public float scale(){ return scene.viewport.sizeNormalized().x; } public void scale(float f){ if (!visible){ return; } scene.viewport.sizeNormalized(f, f); updateViewport(); } public void updateVariables(){ long totalEndTime = TimeUtils.nanoTime(); totalDeltaTime = totalEndTime - totalStartTime; totalStartTime = totalEndTime; tickTimes.remove(0); tickTimes.add(totalDeltaTime); long sumTickTimes = 0; for (long l : tickTimes){ sumTickTimes += l; } avgTickRate = Bdx.TICK_RATE * 1000000000f / sumTickTimes; avgTickTime = 1000 / avgTickRate; if (gl.isEnabled()){ gl.updateStats(); } } private GameObject add(String name, Vector3f position){ GameObject obj = scene.add(name); obj.position(position); obj.parent(display); return obj; } private void updateTickInfo(){ tickInfo.text(formatForSubsystems("tick info", avgTickTime, "ms", avgTickRate, "fps")); } private void updateSubsystems(){ long sumDeltaTimes = 0; HashMap<String, Long> userNanos = new HashMap<String, Long>(); HashMap<String, Float> userPercents = new HashMap<String, Float>(); for (Map.Entry<String, Long> e : deltaTimes.entrySet()){ long deltaTime = e.getValue(); String name = e.getKey(); float deltaTimePercent = 100f * deltaTime / totalDeltaTime; if (name.startsWith("__")){ sumDeltaTimes += deltaTime; percents.put(name, deltaTimePercent); nanos.put(name, deltaTime); }else{ userPercents.put(name, deltaTimePercent); userNanos.put(name, deltaTime); } } long outsideDeltaTime = totalDeltaTime - sumDeltaTimes; float outsideTimePercent = 100f * outsideDeltaTime / totalDeltaTime; nanos.put("__outside", outsideDeltaTime); percents.put("__outside", outsideTimePercent); nanos.putAll(userNanos); percents.putAll(userPercents); startTimes.clear(); deltaTimes.clear(); for (String name : nanos.keySet()){ if (!texts.containsKey(name)){ verticalOffset(1); addTextAndBar(name); scaleBackground(); } String n = name.startsWith("__") ? name.split("__")[1] : name; float m = nanos.get(name) * 0.000001f; float p = percents.get(name); bars.get(name).scale(new Vector3f(BAR_WIDTH * p * 0.01f, BAR_HEIGHT, 1)); texts.get(name).text(formatForSubsystems(n, m, "ms", p, "%")); } } public void updateViewport(float width, float height){ scene.viewport.positionNormalized(0, 1 - scene.viewport.resolution().y * scene.viewport.sizeNormalized().y / height); } public void updateViewport(){ updateViewport(Bdx.display.width(), Bdx.display.height()); } public void updateVisible(){ if (counter >= 1){ counter -= 1; if (tickInfoVisible){ updateTickInfo(); } if (subsystemsVisible){ updateSubsystems(); } if (glVisible){ gl.updateTexts(); } } counter += frequency * Bdx.TICK_TIME; scene.viewport.apply(); } private static String formatForProps(String key, String value){ StringBuffer buffer = new StringBuffer(); addString(buffer, key, 14, false, ' '); buffer.append(" "); int len = value.length(); len = len < 150 ? len : 150; addString(buffer, value, len, false, ' '); return buffer.toString(); } private static String formatForGl(String name, int value){ // "%-21s %7f" StringBuffer buffer = new StringBuffer(); addString(buffer, name, 21, false, ' '); buffer.append(" "); addInt(buffer, value, 7, ' '); return buffer.toString(); } private static String formatForSubsystems(String name, float avgTickTime, String timeUnits, float avgTickRate, String valueUnits){ // "%-14s %4.1f %-3s %4.1f %s" StringBuffer buffer = new StringBuffer(); addString(buffer, name, 14, false, ' '); buffer.append(" "); addFloat(buffer, avgTickTime, 5, 1, ' '); buffer.append(" "); addString(buffer, timeUnits, 3, false, ' '); buffer.append(" "); addFloat(buffer, avgTickRate, 4, 1, ' '); buffer.append(" "); buffer.append(valueUnits); return buffer.toString(); } private static void addInt(StringBuffer buffer, int value, int fieldPadding, char character){ addString(buffer, Integer.toString(value), fieldPadding - 1, true, character); } private static void addFloat(StringBuffer buffer, float value, int fieldPadding, int fractionPadding, char character){ String converted = Float.toString(value); String [] split = converted.split("\\."); addString(buffer, split.length > 0 ? split[0] : "0", fieldPadding - (fractionPadding + 1), true, character); if (fractionPadding > 0){ buffer.append("."); addString(buffer, split.length > 1 ? split[1] : "0", fractionPadding, false, '0'); } } private static void addString(StringBuffer buffer, String value, int padding, boolean padLeft, char character){ if (value != null){ if (value.length() > padding){ buffer.append(value.substring(0, padding)); }else if (padLeft){ padWithCharacter(buffer, padding - value.length(), character); buffer.append(value); }else{ buffer.append(value); padWithCharacter(buffer, padding - value.length(), character); } }else{ padWithCharacter(buffer, padding, character); } } private static void padWithCharacter(StringBuffer buffer, int padding, char character){ for (int i=0; i < padding; i++){ buffer.append(character); } } }