mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-08 12:35:01 +00:00
Add in-game Tracy capture (#2156)
This commit is contained in:
1 parent
233c74e0d7
commit
74ea5c4709
39 files changed
+1492
-210
No files matched your search
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the libTracyClient shared library for the tracy-client module.
|
||||
# Builds the libTracyClient and AngelicaTracyCapture shared libraries for the tracy-client module.
|
||||
set -euo pipefail
|
||||
|
||||
TRACY_VERSION="${TRACY_VERSION:-v0.14.0}"
|
||||
TRACY_VERSION="${TRACY_VERSION:-v0.14.1}"
|
||||
TRACY_VERSION_BARE="${TRACY_VERSION#v}"
|
||||
OUT_DIR="${OUT_DIR:-dist/tracy-natives}"
|
||||
TRACY_REPO="https://github.com/wolfpld/tracy"
|
||||
@@ -31,36 +31,95 @@ else
|
||||
git clone --depth 1 --branch "$TRACY_VERSION" "$TRACY_REPO" "$SRC_DIR"
|
||||
fi
|
||||
|
||||
BUILD_DIR="$WORK_DIR/build"
|
||||
export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-11.0}"
|
||||
|
||||
cmake -S "$REPO_ROOT/tracy-client/src/main/native" -B "$BUILD_DIR" \
|
||||
CLIENT_BUILD_DIR="$WORK_DIR/build-client"
|
||||
cmake -S "$REPO_ROOT/tracy-client/src/main/native" -B "$CLIENT_BUILD_DIR" \
|
||||
-DTRACY_SRC_DIR="$SRC_DIR" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=ON \
|
||||
-DTRACY_STATIC=OFF \
|
||||
-DTRACY_ENABLE=ON \
|
||||
-DTRACY_ON_DEMAND=ON \
|
||||
-DTRACY_DELAYED_INIT=ON \
|
||||
-DTRACY_MANUAL_LIFETIME=ON \
|
||||
-DTRACY_NO_SAMPLING=ON \
|
||||
-DTRACY_NO_SYSTEM_TRACING=ON \
|
||||
-DTRACY_NO_CRASH_HANDLER=ON \
|
||||
-DTRACY_LTO=OFF
|
||||
cmake --build "$BUILD_DIR" --config Release --parallel
|
||||
cmake --build "$CLIENT_BUILD_DIR" --config Release --parallel
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
CPM_CACHE_DIR="$WORK_DIR/cpm"
|
||||
CAPTURE_BUILD_DIR="$WORK_DIR/build-capture"
|
||||
cmake -S "$REPO_ROOT/tracy-client/src/main/native-capture" -B "$CAPTURE_BUILD_DIR" \
|
||||
-DTRACY_SRC_DIR="$SRC_DIR" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCPM_SOURCE_CACHE="$CPM_CACHE_DIR"
|
||||
cmake --build "$CAPTURE_BUILD_DIR" --config Release --parallel
|
||||
|
||||
find_one() {
|
||||
find "$1" -name "$2" 2>/dev/null | head -1
|
||||
}
|
||||
|
||||
CLIENT_LIB_NAME=""
|
||||
CLIENT_LIB=""
|
||||
for NAME in libTracyClient.so libTracyClient.dylib TracyClient.dll; do
|
||||
LIB="$(find "$BUILD_DIR" -name "$NAME" | head -1)"
|
||||
LIB="$(find_one "$CLIENT_BUILD_DIR" "$NAME")"
|
||||
if [[ -n "$LIB" ]]; then
|
||||
cp -L "$LIB" "$OUT_DIR/$NAME"
|
||||
cp "$SRC_DIR/LICENSE" "$OUT_DIR/LICENSE"
|
||||
mkdir -p "$RES_DIR"
|
||||
cp -L "$LIB" "$RES_DIR/$NAME"
|
||||
cp "$SRC_DIR/LICENSE" "$RES_DIR/LICENSE"
|
||||
echo "built: $OUT_DIR/$NAME ($TRACY_VERSION), staged: $RES_DIR/$NAME"
|
||||
exit 0
|
||||
CLIENT_LIB="$LIB"
|
||||
CLIENT_LIB_NAME="$NAME"
|
||||
break
|
||||
fi
|
||||
done
|
||||
echo "error: built library not found under $BUILD_DIR" >&2
|
||||
exit 1
|
||||
if [[ -z "$CLIENT_LIB" ]]; then
|
||||
echo "error: libTracyClient not found under $CLIENT_BUILD_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CAPTURE_LIB_NAME=""
|
||||
CAPTURE_LIB=""
|
||||
for NAME in libAngelicaTracyCapture.so libAngelicaTracyCapture.dylib AngelicaTracyCapture.dll; do
|
||||
LIB="$(find_one "$CAPTURE_BUILD_DIR" "$NAME")"
|
||||
if [[ -n "$LIB" ]]; then
|
||||
CAPTURE_LIB="$LIB"
|
||||
CAPTURE_LIB_NAME="$NAME"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -z "$CAPTURE_LIB" ]]; then
|
||||
echo "error: AngelicaTracyCapture not found under $CAPTURE_BUILD_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STRIPPED_CAPTURE_LIB="$WORK_DIR/$CAPTURE_LIB_NAME"
|
||||
cp -L "$CAPTURE_LIB" "$STRIPPED_CAPTURE_LIB"
|
||||
case "$PLAT_OS" in
|
||||
macos) strip -x "$STRIPPED_CAPTURE_LIB" ;;
|
||||
linux) strip --strip-unneeded "$STRIPPED_CAPTURE_LIB" ;;
|
||||
*) ;;
|
||||
esac
|
||||
|
||||
LICENSE_ZSTD="$(find_one "$CPM_CACHE_DIR/zstd" LICENSE)"
|
||||
LICENSE_CAPSTONE="$(find_one "$CPM_CACHE_DIR/capstone" LICENSE.TXT)"
|
||||
LICENSE_PPQSORT="$(find_one "$CPM_CACHE_DIR/ppqsort" LICENSE)"
|
||||
|
||||
LICENSE_NAMES=(LICENSE LICENSE-zstd LICENSE-capstone LICENSE-ppqsort)
|
||||
LICENSE_SRCS=("$SRC_DIR/LICENSE" "$LICENSE_ZSTD" "$LICENSE_CAPSTONE" "$LICENSE_PPQSORT")
|
||||
|
||||
for i in "${!LICENSE_NAMES[@]}"; do
|
||||
if [[ -z "${LICENSE_SRCS[$i]}" || ! -f "${LICENSE_SRCS[$i]}" ]]; then
|
||||
echo "error: ${LICENSE_NAMES[$i]} source not found under $CPM_CACHE_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
mkdir -p "$OUT_DIR" "$RES_DIR"
|
||||
for DEST in "$OUT_DIR" "$RES_DIR"; do
|
||||
cp -L "$CLIENT_LIB" "$DEST/$CLIENT_LIB_NAME"
|
||||
cp -L "$STRIPPED_CAPTURE_LIB" "$DEST/$CAPTURE_LIB_NAME"
|
||||
for i in "${!LICENSE_NAMES[@]}"; do
|
||||
cp "${LICENSE_SRCS[$i]}" "$DEST/${LICENSE_NAMES[$i]}"
|
||||
done
|
||||
done
|
||||
|
||||
echo "built: $OUT_DIR/$CLIENT_LIB_NAME, $OUT_DIR/$CAPTURE_LIB_NAME ($TRACY_VERSION)"
|
||||
echo "staged: $RES_DIR/$CLIENT_LIB_NAME, $RES_DIR/$CAPTURE_LIB_NAME"
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Build libTracyClient
|
||||
- name: Build Tracy natives
|
||||
shell: bash
|
||||
env:
|
||||
OUT_DIR: dist/tracy-natives
|
||||
|
||||
@@ -40,10 +40,10 @@ public final class SystemProperties {
|
||||
public static final boolean SDL_DISABLE_IN_PASS_CLEAR = Boolean.getBoolean("angelica.sdlgpu.disableInPassClear");
|
||||
|
||||
// Tracy
|
||||
public static final boolean TRACY = Boolean.getBoolean("angelica.tracy");
|
||||
public static final boolean TRACY_FINE_ZONES = Boolean.getBoolean("angelica.tracy.fineZones");
|
||||
public static final Boolean TRACY_OVERRIDE = parseBooleanOverride("angelica.tracy");
|
||||
public static final Boolean TRACY_FINE_ZONES_OVERRIDE = parseBooleanOverride("angelica.tracy.fineZones");
|
||||
public static final String TRACY_DIR = System.getProperty("angelica.tracy.dir", "angelica" + File.separator + "natives" + File.separator + "tracy");
|
||||
public static final int TRACY_MAX_SRC_LOCS = Math.max(16, Integer.getInteger("angelica.tracy.maxSrcLocs", 4096));
|
||||
public static final Integer TRACY_MAX_SRC_LOCS_OVERRIDE = Integer.getInteger("angelica.tracy.maxSrcLocs");
|
||||
|
||||
// Profiling
|
||||
public static final String PROFILE_OPTS = System.getProperty("angelica.profile.opts", "event=wall,interval=5ms,alloc=512k,lock=10ms");
|
||||
@@ -152,6 +152,11 @@ public final class SystemProperties {
|
||||
}
|
||||
}
|
||||
|
||||
private static Boolean parseBooleanOverride(String key) {
|
||||
final String raw = System.getProperty(key);
|
||||
return raw == null ? null : Boolean.parseBoolean(raw);
|
||||
}
|
||||
|
||||
private static double parseDouble(String key) {
|
||||
final String raw = System.getProperty(key);
|
||||
if (raw == null || raw.isEmpty()) return 0.0D;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.gtnewhorizons.angelica.glsm.profiling;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.CaptureGate;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.sun.management.ThreadMXBean;
|
||||
@@ -66,12 +65,12 @@ public final class Tracy {
|
||||
|
||||
static {
|
||||
TracyBackend backend = null;
|
||||
if (SystemProperties.TRACY) {
|
||||
if (TracyOptions.enabled()) {
|
||||
backend = loadAndInit();
|
||||
}
|
||||
BACKEND = backend;
|
||||
ENABLED = backend != null;
|
||||
FINE_ZONES = ENABLED && SystemProperties.TRACY_FINE_ZONES;
|
||||
FINE_ZONES = ENABLED && TracyOptions.fineZones();
|
||||
if (ENABLED) {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(Tracy::shutdown, "Tracy-Shutdown"));
|
||||
SECTIONS.registerCategory(SECTION_WORLD, "World");
|
||||
@@ -86,6 +85,18 @@ public final class Tracy {
|
||||
|
||||
private static void shutdown() {
|
||||
SHUTTING_DOWN = true;
|
||||
if (isCaptureActive(BACKEND.captureState())) {
|
||||
BACKEND.captureStop();
|
||||
final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
|
||||
while (isCaptureActive(BACKEND.captureState()) && System.nanoTime() < deadline) {
|
||||
try {
|
||||
Thread.sleep(50);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
final Thread worker = new Thread(BACKEND::shutdown, "Tracy-Shutdown-Worker");
|
||||
worker.setDaemon(true);
|
||||
worker.start();
|
||||
@@ -96,6 +107,10 @@ public final class Tracy {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isCaptureActive(int state) {
|
||||
return state == TracyBackend.CAPTURE_CONNECTING || state == TracyBackend.CAPTURE_RECORDING || state == TracyBackend.CAPTURE_SAVING;
|
||||
}
|
||||
|
||||
private static TracyBackend loadAndInit() {
|
||||
TracyBackend found = null;
|
||||
try {
|
||||
@@ -104,11 +119,11 @@ public final class Tracy {
|
||||
found = it.next();
|
||||
}
|
||||
} catch (ServiceConfigurationError | LinkageError e) {
|
||||
LOGGER.warn("Tracy requested (-Dangelica.tracy=true) but backend unavailable: {}", e.getMessage());
|
||||
LOGGER.warn("Tracy: backend unavailable: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
if (found == null) {
|
||||
LOGGER.warn("Tracy requested (-Dangelica.tracy=true) but no TracyBackend service present");
|
||||
LOGGER.warn("Tracy: enabled but the angelica-tracy jar is not installed");
|
||||
return null;
|
||||
}
|
||||
if (!implementsCurrentAbi(found)) return null;
|
||||
@@ -120,7 +135,7 @@ public final class Tracy {
|
||||
init.start();
|
||||
try {
|
||||
if (!done.await(15, TimeUnit.SECONDS) && result.compareAndSet(null, Boolean.FALSE)) {
|
||||
LOGGER.warn("Tracy requested (-Dangelica.tracy=true) but init timed out; profiling disabled");
|
||||
LOGGER.warn("Tracy: init timed out; profiling disabled");
|
||||
return null;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
@@ -131,7 +146,7 @@ public final class Tracy {
|
||||
return null;
|
||||
}
|
||||
if (!Boolean.TRUE.equals(result.get())) {
|
||||
LOGGER.warn("Tracy requested (-Dangelica.tracy=true) but native init failed; profiling disabled");
|
||||
LOGGER.warn("Tracy: native init failed; profiling disabled");
|
||||
return null;
|
||||
}
|
||||
return backend;
|
||||
@@ -388,6 +403,31 @@ public final class Tracy {
|
||||
return ENABLED && BACKEND.isConnected();
|
||||
}
|
||||
|
||||
public static String captureStart(String path, int seconds) {
|
||||
if (!ENABLED) return "Tracy not enabled";
|
||||
return BACKEND.captureStart(path, seconds);
|
||||
}
|
||||
|
||||
public static void captureStop() {
|
||||
if (!ENABLED) return;
|
||||
BACKEND.captureStop();
|
||||
}
|
||||
|
||||
public static int captureState() {
|
||||
if (!ENABLED) return 0;
|
||||
return BACKEND.captureState();
|
||||
}
|
||||
|
||||
public static long captureElapsedMs() {
|
||||
if (!ENABLED) return 0;
|
||||
return BACKEND.captureElapsedMs();
|
||||
}
|
||||
|
||||
public static String captureError() {
|
||||
if (!ENABLED) return "";
|
||||
return BACKEND.captureError();
|
||||
}
|
||||
|
||||
private static boolean gpuInitAttempted;
|
||||
private static boolean gpuReady;
|
||||
@Setter private static boolean gpuZonesEnabled = true;
|
||||
|
||||
@@ -12,6 +12,13 @@ public interface TracyBackend {
|
||||
int SEVERITY_ERROR = 4;
|
||||
int SEVERITY_FATAL = 5;
|
||||
|
||||
int CAPTURE_IDLE = 0;
|
||||
int CAPTURE_CONNECTING = 1;
|
||||
int CAPTURE_RECORDING = 2;
|
||||
int CAPTURE_SAVING = 3;
|
||||
int CAPTURE_DONE = 4;
|
||||
int CAPTURE_FAILED = 5;
|
||||
|
||||
boolean init();
|
||||
|
||||
long internSrcLoc(String name, int color);
|
||||
@@ -47,4 +54,12 @@ public interface TracyBackend {
|
||||
boolean gpuBeginZone(long srcLoc);
|
||||
void gpuEndZone();
|
||||
void gpuCollect();
|
||||
|
||||
default String captureStart(String path, int seconds) {
|
||||
return "Tracy capture unsupported by this angelica-tracy build";
|
||||
}
|
||||
default void captureStop() {}
|
||||
default int captureState() { return CAPTURE_IDLE; }
|
||||
default long captureElapsedMs() { return 0; }
|
||||
default String captureError() { return ""; }
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.gtnewhorizons.angelica.glsm.profiling;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
|
||||
public final class TracyOptions {
|
||||
|
||||
private static boolean configEnabled;
|
||||
private static boolean configRemote;
|
||||
private static boolean configFineZones;
|
||||
private static int configMaxSrcLocs = 4096;
|
||||
|
||||
private TracyOptions() {}
|
||||
|
||||
public static void latch(boolean enabled, boolean allowRemote, boolean fineZones, int maxSrcLocs) {
|
||||
configEnabled = enabled;
|
||||
configRemote = allowRemote;
|
||||
configFineZones = fineZones;
|
||||
configMaxSrcLocs = maxSrcLocs;
|
||||
}
|
||||
|
||||
public static boolean enabled() {
|
||||
return resolveBoolean(SystemProperties.TRACY_OVERRIDE, configEnabled);
|
||||
}
|
||||
|
||||
public static boolean allowRemote() {
|
||||
return configRemote;
|
||||
}
|
||||
|
||||
public static boolean fineZones() {
|
||||
return resolveBoolean(SystemProperties.TRACY_FINE_ZONES_OVERRIDE, configFineZones);
|
||||
}
|
||||
|
||||
public static int maxSrcLocs() {
|
||||
return resolveMaxSrcLocs(SystemProperties.TRACY_MAX_SRC_LOCS_OVERRIDE, configMaxSrcLocs);
|
||||
}
|
||||
|
||||
public static boolean configEnabled() {
|
||||
return configEnabled;
|
||||
}
|
||||
|
||||
public static boolean configRemote() {
|
||||
return configRemote;
|
||||
}
|
||||
|
||||
public static boolean configFineZones() {
|
||||
return configFineZones;
|
||||
}
|
||||
|
||||
public static int configMaxSrcLocs() {
|
||||
return configMaxSrcLocs;
|
||||
}
|
||||
|
||||
public static boolean backendPresent() {
|
||||
return TracyOptions.class.getResource("/com/gtnewhorizons/angelica/tracy/TracyClientBackend.class") != null;
|
||||
}
|
||||
|
||||
static boolean resolveBoolean(Boolean override, boolean config) {
|
||||
return override != null ? override : config;
|
||||
}
|
||||
|
||||
static int resolveMaxSrcLocs(Integer override, int config) {
|
||||
return Math.max(16, override != null ? override : config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.gtnewhorizons.angelica.glsm.profiling;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class TracyOptionsTest {
|
||||
|
||||
@Test
|
||||
void resolveBooleanFallsBackToConfigWhenNoOverride() {
|
||||
assertTrue(TracyOptions.resolveBoolean(null, true));
|
||||
assertFalse(TracyOptions.resolveBoolean(null, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveBooleanOverrideWinsEitherWay() {
|
||||
assertTrue(TracyOptions.resolveBoolean(Boolean.TRUE, false));
|
||||
assertFalse(TracyOptions.resolveBoolean(Boolean.FALSE, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMaxSrcLocsFallsBackToConfigWhenNoOverride() {
|
||||
assertEquals(4096, TracyOptions.resolveMaxSrcLocs(null, 4096));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMaxSrcLocsOverrideWins() {
|
||||
assertEquals(8192, TracyOptions.resolveMaxSrcLocs(8192, 4096));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMaxSrcLocsClampsBothConfigAndOverrideToTheMinimum() {
|
||||
assertEquals(16, TracyOptions.resolveMaxSrcLocs(null, 4));
|
||||
assertEquals(16, TracyOptions.resolveMaxSrcLocs(1, 4096));
|
||||
}
|
||||
|
||||
@Test
|
||||
void latchExposesConfigValuesThroughTheAccessors() {
|
||||
TracyOptions.latch(true, true, true, 8192);
|
||||
assertTrue(TracyOptions.configEnabled());
|
||||
assertTrue(TracyOptions.configRemote());
|
||||
assertTrue(TracyOptions.configFineZones());
|
||||
assertEquals(8192, TracyOptions.configMaxSrcLocs());
|
||||
|
||||
TracyOptions.latch(false, false, false, 4096);
|
||||
assertFalse(TracyOptions.configEnabled());
|
||||
assertFalse(TracyOptions.configRemote());
|
||||
assertFalse(TracyOptions.configFineZones());
|
||||
assertEquals(4096, TracyOptions.configMaxSrcLocs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowRemoteHasNoJvmOverrideAndTracksConfigDirectly() {
|
||||
TracyOptions.latch(false, true, false, 4096);
|
||||
assertTrue(TracyOptions.allowRemote());
|
||||
|
||||
TracyOptions.latch(false, false, false, 4096);
|
||||
assertFalse(TracyOptions.allowRemote());
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ eventbus = "7.0-beta.15"
|
||||
glslTransformLib = "0.2.0-33.g3ec381e"
|
||||
antlr4Runtime = "4.13.2"
|
||||
jcpp = "1.4.14"
|
||||
tracy = "0.14.0"
|
||||
tracy = "0.14.1"
|
||||
asyncProfiler = "4.4"
|
||||
retroFuturaBootstrap = "1.1.0"
|
||||
|
||||
|
||||
@@ -104,6 +104,9 @@ if [ -n "$JFR_OUT" ]; then
|
||||
PROFILE_ARGS+=(-Dangelica.profile.opts="$ASPROF_OPTS")
|
||||
fi
|
||||
fi
|
||||
if [ "$MODE" = "tracy" ]; then
|
||||
PROFILE_ARGS+=(-Dangelica.tracy=true)
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "tracy" ]; then
|
||||
if ! command -v "$TRACY_CAPTURE" >/dev/null 2>&1 && [ ! -x "$TRACY_CAPTURE" ]; then
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.gtnewhorizons.angelica;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.proxy.CommonProxy;
|
||||
import cpw.mods.fml.common.Mod;
|
||||
import cpw.mods.fml.common.SidedProxy;
|
||||
@@ -32,7 +31,7 @@ public class AngelicaMod {
|
||||
public static final String MOD_ID = "angelica";
|
||||
public static final Logger LOGGER = LogManager.getLogger("Angelica");
|
||||
public static final boolean lwjglDebug = SystemProperties.LWJGL_DEBUG;
|
||||
public static final boolean tracyEnabled = Tracy.ENABLED;
|
||||
public static final String KEY_CATEGORY = "key.categories.angelica";
|
||||
|
||||
@SidedProxy(
|
||||
clientSide = "com.gtnewhorizons.angelica.proxy.ClientProxy",
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.gtnewhorizons.angelica.client.gui;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.debug.profiling.TracyCaptureNotifier;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.TracyBackend;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.TracyOptions;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.ActionOption;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.OptionFlag;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.OptionGroup;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.OptionImpl;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.OptionPage;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.control.ControlValueFormatter;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.control.SliderControl;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.control.TickBoxControl;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.storage.AngelicaOptionsStorage;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class TracyOptionPages {
|
||||
|
||||
private static final AngelicaOptionsStorage angelicaOpts = new AngelicaOptionsStorage();
|
||||
|
||||
public static OptionPage tracy() {
|
||||
final String stopLabel = I18n.format("options.angelica.tracy.captureAction.stop");
|
||||
final String savingLabel = I18n.format("options.angelica.tracy.captureAction.saving");
|
||||
final String startLabel = I18n.format("options.angelica.tracy.captureAction.start");
|
||||
final Supplier<String> captureActionLabel = () -> switch (Tracy.captureState()) {
|
||||
case TracyBackend.CAPTURE_CONNECTING, TracyBackend.CAPTURE_RECORDING -> stopLabel;
|
||||
case TracyBackend.CAPTURE_SAVING -> savingLabel;
|
||||
default -> startLabel;
|
||||
};
|
||||
|
||||
final String restartLabel = I18n.format("options.angelica.tracy.status.restart");
|
||||
final String runningLabel = I18n.format("options.angelica.tracy.status.running");
|
||||
final String runningForcedLabel = I18n.format("options.angelica.tracy.status.runningForced");
|
||||
final String failedLabel = I18n.format("options.angelica.tracy.status.failed");
|
||||
final String offLabel = I18n.format("options.angelica.tracy.status.off");
|
||||
final Supplier<String> statusLabel = () -> {
|
||||
if (AngelicaConfig.enableTracy != TracyOptions.configEnabled()
|
||||
|| AngelicaConfig.tracyAllowRemote != TracyOptions.configRemote()
|
||||
|| AngelicaConfig.tracyFineZones != TracyOptions.configFineZones()
|
||||
|| AngelicaConfig.tracyMaxSrcLocs != TracyOptions.configMaxSrcLocs()) {
|
||||
return restartLabel;
|
||||
}
|
||||
if (Tracy.ENABLED) {
|
||||
return SystemProperties.TRACY_OVERRIDE != null ? runningForcedLabel : runningLabel;
|
||||
}
|
||||
return TracyOptions.enabled() ? failedLabel : offLabel;
|
||||
};
|
||||
|
||||
return new OptionPage(I18n.format("options.angelica.tracy.page"), ImmutableList.of(
|
||||
OptionGroup.createBuilder()
|
||||
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
|
||||
.setName(I18n.format("options.angelica.tracy.enable"))
|
||||
.setTooltip(overrideTooltip("options.angelica.tracy.enable.tooltip", "angelica.tracy", SystemProperties.TRACY_OVERRIDE != null))
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((opts, value) -> AngelicaConfig.enableTracy = value, opts -> AngelicaConfig.enableTracy)
|
||||
.setEnabled(SystemProperties.TRACY_OVERRIDE == null)
|
||||
.setFlags(OptionFlag.REQUIRES_GAME_RESTART)
|
||||
.build())
|
||||
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
|
||||
.setName(I18n.format("options.angelica.tracy.allowRemote"))
|
||||
.setTooltip(I18n.format("options.angelica.tracy.allowRemote.tooltip"))
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((opts, value) -> AngelicaConfig.tracyAllowRemote = value, opts -> AngelicaConfig.tracyAllowRemote)
|
||||
.setFlags(OptionFlag.REQUIRES_GAME_RESTART)
|
||||
.build())
|
||||
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
|
||||
.setName(I18n.format("options.angelica.tracy.fineZones"))
|
||||
.setTooltip(overrideTooltip("options.angelica.tracy.fineZones.tooltip", "angelica.tracy.fineZones", SystemProperties.TRACY_FINE_ZONES_OVERRIDE != null))
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((opts, value) -> AngelicaConfig.tracyFineZones = value, opts -> AngelicaConfig.tracyFineZones)
|
||||
.setEnabled(SystemProperties.TRACY_FINE_ZONES_OVERRIDE == null)
|
||||
.setFlags(OptionFlag.REQUIRES_GAME_RESTART)
|
||||
.build())
|
||||
.add(OptionImpl.createBuilder(int.class, angelicaOpts)
|
||||
.setName(I18n.format("options.angelica.tracy.maxSrcLocs"))
|
||||
.setTooltip(overrideTooltip("options.angelica.tracy.maxSrcLocs.tooltip", "angelica.tracy.maxSrcLocs", SystemProperties.TRACY_MAX_SRC_LOCS_OVERRIDE != null))
|
||||
.setControl(o -> new SliderControl(o, 1024, 16384, 1024, ControlValueFormatter.number()))
|
||||
.setBinding((opts, value) -> AngelicaConfig.tracyMaxSrcLocs = value, opts -> AngelicaConfig.tracyMaxSrcLocs)
|
||||
.setEnabled(SystemProperties.TRACY_MAX_SRC_LOCS_OVERRIDE == null)
|
||||
.setFlags(OptionFlag.REQUIRES_GAME_RESTART)
|
||||
.build())
|
||||
.build(),
|
||||
OptionGroup.createBuilder()
|
||||
.add(new ActionOption(I18n.format("options.angelica.tracy.status"), I18n.format("options.angelica.tracy.status.tooltip"),
|
||||
statusLabel, () -> {}, () -> false))
|
||||
.build(),
|
||||
OptionGroup.createBuilder()
|
||||
.add(OptionImpl.createBuilder(int.class, angelicaOpts)
|
||||
.setName(I18n.format("options.angelica.tracy.captureLength"))
|
||||
.setTooltip(I18n.format("options.angelica.tracy.captureLength.tooltip"))
|
||||
.setControl(o -> new SliderControl(o, 10, 300, 10, ControlValueFormatter.seconds()))
|
||||
.setBinding((opts, value) -> AngelicaConfig.tracyCaptureSeconds = value, opts -> AngelicaConfig.tracyCaptureSeconds)
|
||||
.setEnabled(Tracy.ENABLED)
|
||||
.build())
|
||||
.add(new ActionOption(I18n.format("options.angelica.tracy.captureAction"), I18n.format("options.angelica.tracy.captureAction.tooltip"),
|
||||
captureActionLabel, TracyCaptureNotifier.INSTANCE::toggle, () -> Tracy.ENABLED))
|
||||
.build()));
|
||||
}
|
||||
|
||||
private static String overrideTooltip(String baseKey, String flag, boolean overridden) {
|
||||
final String base = I18n.format(baseKey);
|
||||
return overridden ? base + " " + I18n.format("options.angelica.tracy.overriddenBy", flag) : base;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@ package com.gtnewhorizons.angelica.commands;
|
||||
|
||||
// Debug commands adapted from Beddium by Ven and FalsePattern
|
||||
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.debug.ChunkDebugMinimap;
|
||||
import com.gtnewhorizons.angelica.debug.flyby.FlybyRoute;
|
||||
import com.gtnewhorizons.angelica.debug.flyby.FlybyRunner;
|
||||
import com.gtnewhorizons.angelica.debug.profiling.AsprofRecorder;
|
||||
import com.gtnewhorizons.angelica.debug.profiling.TracyCaptureNotifier;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.TracyBackend;
|
||||
import com.gtnewhorizons.angelica.rendering.RenderRecovery;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasDebugScreenHandler;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasWorldRenderer;
|
||||
@@ -19,23 +23,42 @@ import net.minecraft.util.ChatComponentText;
|
||||
import net.minecraft.util.EnumChatFormatting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AngelicaCommand extends CommandBase {
|
||||
|
||||
private static final List<String> SUBCOMMANDS = buildSubcommands();
|
||||
private static final Map<String, String> HELP = buildHelp();
|
||||
private static final String[] SUBCOMMANDS = buildSubcommands();
|
||||
private static final String USAGE = "/angelica <" + String.join("|", SUBCOMMANDS) + ">";
|
||||
|
||||
private static List<String> buildSubcommands() {
|
||||
final List<String> subcommands = new ArrayList<>(Arrays.asList("wireframe", "fog", "minimap", "flyby", "profile", "help"));
|
||||
if (isDeobfuscatedEnvironment()) {
|
||||
subcommands.add("crashtest");
|
||||
private static Map<String, String> buildHelp() {
|
||||
final Map<String, String> help = new LinkedHashMap<>();
|
||||
if (SystemProperties.debugTooling()) {
|
||||
help.put("wireframe", helpLine("wireframe", "Toggle wireframe rendering"));
|
||||
help.put("fog", helpLine("fog", "Toggle fog debug on F3"));
|
||||
help.put("minimap", helpLine("minimap", "Toggle chunk debug overlay"));
|
||||
help.put("flyby", helpLine("flyby <" + FlybyRoute.ids() + ">", "Run a deterministic benchmark route"));
|
||||
help.put("profile", helpLine("profile <start|stop|status>", "Control async-profiler (JFR) recording"));
|
||||
if (SystemProperties.isDeobf()) {
|
||||
help.put("crashtest", helpLine("crashtest", "Arm a crash on the next tile entity render"));
|
||||
}
|
||||
}
|
||||
if (Tracy.ENABLED) {
|
||||
help.put("tracy", helpLine("tracy <start|stop|status>", "Control Tracy capture recording"));
|
||||
}
|
||||
return help;
|
||||
}
|
||||
|
||||
private static String[] buildSubcommands() {
|
||||
final String[] subcommands = HELP.keySet().toArray(new String[HELP.size() + 1]);
|
||||
subcommands[HELP.size()] = "help";
|
||||
return subcommands;
|
||||
}
|
||||
|
||||
private static boolean isDeobfuscatedEnvironment() {
|
||||
return SystemProperties.isDeobf();
|
||||
private static String helpLine(String syntax, String description) {
|
||||
return EnumChatFormatting.GRAY + " /angelica " + syntax + EnumChatFormatting.WHITE + " - " + description;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,7 +68,7 @@ public class AngelicaCommand extends CommandBase {
|
||||
|
||||
@Override
|
||||
public String getCommandUsage(ICommandSender sender) {
|
||||
return "/angelica <wireframe|fog|minimap|flyby|profile" + (isDeobfuscatedEnvironment() ? "|crashtest" : "") + "|help>";
|
||||
return USAGE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -73,7 +96,7 @@ public class AngelicaCommand extends CommandBase {
|
||||
@Override
|
||||
public List<String> addTabCompletionOptions(ICommandSender sender, String[] args) {
|
||||
if (args.length == 1) {
|
||||
return getListOfStringsMatchingLastWord(args, SUBCOMMANDS.toArray(new String[0]));
|
||||
return getListOfStringsMatchingLastWord(args, SUBCOMMANDS);
|
||||
}
|
||||
if (args.length == 2 && "flyby".equalsIgnoreCase(args[0])) {
|
||||
final List<String> options = new ArrayList<>();
|
||||
@@ -84,7 +107,10 @@ public class AngelicaCommand extends CommandBase {
|
||||
return getListOfStringsMatchingLastWord(args, options.toArray(new String[0]));
|
||||
}
|
||||
if (args.length == 2 && "profile".equalsIgnoreCase(args[0])) {
|
||||
return getListOfStringsMatchingLastWord(args, new String[] { "start", "stop", "status" });
|
||||
return getListOfStringsMatchingLastWord(args, "start", "stop", "status");
|
||||
}
|
||||
if (args.length == 2 && "tracy".equalsIgnoreCase(args[0])) {
|
||||
return getListOfStringsMatchingLastWord(args, "start", "stop", "status");
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
@@ -97,6 +123,10 @@ public class AngelicaCommand extends CommandBase {
|
||||
}
|
||||
|
||||
final String subcommand = args[0].toLowerCase();
|
||||
if (!HELP.containsKey(subcommand)) {
|
||||
sendHelp(sender);
|
||||
return;
|
||||
}
|
||||
if (requiresCheats(subcommand) && !cheatsAllowed()) {
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "[Angelica] " + subcommand + " needs cheats enabled on a world you host"));
|
||||
return;
|
||||
@@ -108,14 +138,8 @@ public class AngelicaCommand extends CommandBase {
|
||||
case "minimap" -> handleMinimap(sender);
|
||||
case "flyby" -> handleFlyby(sender, args);
|
||||
case "profile" -> handleProfile(sender, args);
|
||||
case "crashtest" -> {
|
||||
if (isDeobfuscatedEnvironment()) {
|
||||
handleCrashTest(sender);
|
||||
} else {
|
||||
sendHelp(sender);
|
||||
}
|
||||
}
|
||||
default -> sendHelp(sender);
|
||||
case "tracy" -> handleTracy(sender, args);
|
||||
case "crashtest" -> handleCrashTest(sender);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,15 +263,75 @@ public class AngelicaCommand extends CommandBase {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTracy(ICommandSender sender, String[] args) {
|
||||
if (args.length < 2) {
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + "Usage: /angelica tracy <start|stop|status> [seconds]"));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (args[1].toLowerCase()) {
|
||||
case "start" -> {
|
||||
int seconds = AngelicaConfig.tracyCaptureSeconds;
|
||||
if (args.length > 2) {
|
||||
try {
|
||||
seconds = Integer.parseInt(args[2]);
|
||||
} catch (NumberFormatException e) {
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + "Usage: /angelica tracy start [seconds]"));
|
||||
return;
|
||||
}
|
||||
if (seconds < 0) {
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + "Usage: /angelica tracy start [seconds]"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
final String error = TracyCaptureNotifier.INSTANCE.startCapture(seconds);
|
||||
if (error != null) {
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "[Angelica] " + error));
|
||||
} else {
|
||||
final String length = seconds > 0 ? seconds + "s" : "until stopped";
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + "Tracy capture started (" + length + "): " + TracyCaptureNotifier.INSTANCE.path()));
|
||||
}
|
||||
}
|
||||
case "stop" -> {
|
||||
final int state = Tracy.captureState();
|
||||
if (state == TracyBackend.CAPTURE_CONNECTING || state == TracyBackend.CAPTURE_RECORDING) {
|
||||
Tracy.captureStop();
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + "Stopping Tracy capture, saving..."));
|
||||
} else if (state == TracyBackend.CAPTURE_SAVING) {
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "[Angelica] Tracy capture is already saving"));
|
||||
} else {
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "[Angelica] No Tracy capture is running"));
|
||||
}
|
||||
}
|
||||
case "status" -> {
|
||||
final int state = Tracy.captureState();
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + "Tracy capture: " + tracyStateName(state) + " (" + Tracy.captureElapsedMs() / 1000L + "s)"));
|
||||
if (state == TracyBackend.CAPTURE_FAILED) {
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + " " + Tracy.captureError()));
|
||||
} else if (TracyCaptureNotifier.INSTANCE.path() != null) {
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + " " + TracyCaptureNotifier.INSTANCE.path()));
|
||||
}
|
||||
}
|
||||
default -> sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + "Usage: /angelica tracy <start|stop|status> [seconds]"));
|
||||
}
|
||||
}
|
||||
|
||||
private static String tracyStateName(int state) {
|
||||
return switch (state) {
|
||||
case TracyBackend.CAPTURE_IDLE -> "idle";
|
||||
case TracyBackend.CAPTURE_CONNECTING -> "connecting";
|
||||
case TracyBackend.CAPTURE_RECORDING -> "recording";
|
||||
case TracyBackend.CAPTURE_SAVING -> "saving";
|
||||
case TracyBackend.CAPTURE_DONE -> "done";
|
||||
case TracyBackend.CAPTURE_FAILED -> "failed";
|
||||
default -> "unknown";
|
||||
};
|
||||
}
|
||||
|
||||
private void sendHelp(ICommandSender sender) {
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] Debug Commands:"));
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + " /angelica wireframe" + EnumChatFormatting.WHITE + " - Toggle wireframe rendering"));
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + " /angelica fog" + EnumChatFormatting.WHITE + " - Toggle fog debug on F3"));
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + " /angelica minimap" + EnumChatFormatting.WHITE + " - Toggle chunk debug overlay"));
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + " /angelica flyby <" + FlybyRoute.ids() + ">" + EnumChatFormatting.WHITE + " - Run a deterministic benchmark route"));
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + " /angelica profile <start|stop|status>" + EnumChatFormatting.WHITE + " - Control async-profiler (JFR) recording"));
|
||||
if (isDeobfuscatedEnvironment()) {
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + " /angelica crashtest" + EnumChatFormatting.WHITE + " - Arm a crash on the next tile entity render"));
|
||||
for (String line : HELP.values()) {
|
||||
sender.addChatMessage(new ChatComponentText(line));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -472,6 +472,32 @@ public class AngelicaConfig {
|
||||
"minecraft:andesite", "minecraft:dirt", "minecraft:granite", "minecraft:grass", "minecraft:mycelium", "minecraft:sand", "minecraft:soul_sand", "etfuturum:calcite", "etfuturum:coarse_dirt", "etfuturum:concrete_powder", "etfuturum:grass_path", "BiomesOPlenty:ash", "BiomesOPlenty:driedDirt", "BiomesOPlenty:hardDirt", "BiomesOPlenty:hardSand", "BiomesOPlenty:mud", "BiomesOPlenty:newBopDirt", "Botania:dirtPath", "Botania:enchantedSoil", "Botania:livingrock", "Botania:prismarine", "Botania:shimmerrock", "Botany:loam", "Botany:loamNoWeed", "Botany:soil", "Botany:soilNoWeed", "chisel:moss", "chisel:moss_carpet", "ExtraUtilities:color_hellsand", "ExtraUtilities:cursedearthside", "GalaxySpace:acentauribbgrunt", "GalaxySpace:acentauribbsubgrunt", "GalaxySpace:barnardaCdirt", "GalaxySpace:barnardaEgrunt", "GalaxySpace:barnardaEsubgrunt", "GalaxySpace:barnardaFgrunt", "GalaxySpace:barnardaFsubgrunt", "GalaxySpace:callistoblocks", "GalaxySpace:ceresblocks", "GalaxySpace:deimosblocks", "GalaxySpace:europagrunt", "GalaxySpace:ganymedeblocks", "GalaxySpace:haumeablocks", "GalaxySpace:ioblocks", "GalaxySpace:makemakegrunt", "GalaxySpace:mercuryblocks", "GalaxySpace:mirandablocks", "GalaxySpace:oberonblocks", "GalaxySpace:phobosblocks", "GalaxySpace:proteusblocks", "GalaxySpace:tcetieblocks", "GalaxySpace:titanblocks", "GalaxySpace:tritonblocks", "GalaxySpace:vegabgrunt", "GalaxySpace:vegabsubgrunt", "GalaxySpace:venusblocks", "gregtech:gt.blockgranites", "IC2:blockBasalt", "MagicBees:magicbees.enchantedEarth", "RandomThings:fertilizedDirt", "ToxicEverglades:blockDarkWorldGround2", "VillageNames:concretePowder", "witchery:pitdirt"
|
||||
};
|
||||
|
||||
@Config.Comment("Enable the Tracy profiler backend. Requires angelica-tracy.jar on the classpath. Overridden by -Dangelica.tracy")
|
||||
@Config.DefaultBoolean(false)
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean enableTracy;
|
||||
|
||||
@Config.Comment("Allow Tracy to accept connections from other machines, not just localhost")
|
||||
@Config.DefaultBoolean(false)
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean tracyAllowRemote;
|
||||
|
||||
@Config.Comment("Emit Tracy zones for many more call sites. More detail, more overhead. Overridden by -Dangelica.tracy.fineZones")
|
||||
@Config.DefaultBoolean(false)
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean tracyFineZones;
|
||||
|
||||
@Config.Comment("Max distinct Tracy zone/message call sites to preallocate. Overridden by -Dangelica.tracy.maxSrcLocs")
|
||||
@Config.DefaultInt(4096)
|
||||
@Config.RangeInt(min = 1024, max = 16384)
|
||||
@Config.RequiresMcRestart
|
||||
public static int tracyMaxSrcLocs;
|
||||
|
||||
@Config.Comment("Default length in seconds for a Tracy capture started from the command or video settings. 0 = until stopped")
|
||||
@Config.DefaultInt(60)
|
||||
@Config.RangeInt(min = 0, max = 3600)
|
||||
public static int tracyCaptureSeconds;
|
||||
|
||||
public static void applyGpuCullingMode() {
|
||||
GpuCulling.setMode(gpuCullingMode == null ? GpuCullingMode.CPU_ONLY : gpuCullingMode);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public final class AsprofRecorder {
|
||||
if (!available()) return "async-profiler is not available on this platform/build";
|
||||
if (SESSION.recordingId() != 0) return "already recording to " + SESSION.outputPath();
|
||||
|
||||
File file = SystemProperties.PROFILE_OUTPUT.isEmpty() ? new File(SystemProperties.PROFILE_DIR, fileName(tag, System.currentTimeMillis())) : new File(SystemProperties.PROFILE_OUTPUT);
|
||||
File file = SystemProperties.PROFILE_OUTPUT.isEmpty() ? new File(SystemProperties.PROFILE_DIR, fileName(tag, "jfr", System.currentTimeMillis())) : new File(SystemProperties.PROFILE_OUTPUT);
|
||||
file = file.getAbsoluteFile();
|
||||
if (file.exists()) return "output file already exists: " + file.getPath();
|
||||
final File parent = file.getParentFile();
|
||||
@@ -121,9 +121,9 @@ public final class AsprofRecorder {
|
||||
return opts.isEmpty() ? "start,jfr,file=" + file : "start,jfr," + opts + ",file=" + file;
|
||||
}
|
||||
|
||||
static String fileName(String tag, long nowMillis) {
|
||||
static String fileName(String tag, String ext, long nowMillis) {
|
||||
final SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.ROOT);
|
||||
return "angelica-" + tag + "-" + format.format(new Date(nowMillis)) + ".jfr";
|
||||
return "angelica-" + tag + "-" + format.format(new Date(nowMillis)) + "." + ext;
|
||||
}
|
||||
|
||||
static String firstLine(String s) {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.gtnewhorizons.angelica.debug.profiling;
|
||||
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.TracyBackend;
|
||||
import cpw.mods.fml.client.registry.ClientRegistry;
|
||||
import cpw.mods.fml.common.FMLCommonHandler;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import cpw.mods.fml.common.gameevent.TickEvent;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.settings.KeyBinding;
|
||||
import net.minecraft.util.ChatComponentText;
|
||||
import net.minecraft.util.EnumChatFormatting;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class TracyCaptureNotifier {
|
||||
private static final Logger LOGGER = LogManager.getLogger("Angelica/Tracy");
|
||||
|
||||
public static final TracyCaptureNotifier INSTANCE = new TracyCaptureNotifier();
|
||||
|
||||
private File file;
|
||||
private boolean inFlight;
|
||||
private KeyBinding captureKey;
|
||||
|
||||
private TracyCaptureNotifier() {}
|
||||
|
||||
public void register() {
|
||||
this.captureKey = new KeyBinding("angelica.keybind.tracyCapture", 0, AngelicaMod.KEY_CATEGORY);
|
||||
ClientRegistry.registerKeyBinding(this.captureKey);
|
||||
FMLCommonHandler.instance().bus().register(this);
|
||||
}
|
||||
|
||||
public void toggle() {
|
||||
final int state = Tracy.captureState();
|
||||
if (state == TracyBackend.CAPTURE_CONNECTING || state == TracyBackend.CAPTURE_RECORDING) {
|
||||
Tracy.captureStop();
|
||||
} else if (state != TracyBackend.CAPTURE_SAVING) {
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
final String error = this.startCapture(AngelicaConfig.tracyCaptureSeconds);
|
||||
if (error != null) {
|
||||
report(mc, true, "Tracy capture failed to start: " + error);
|
||||
} else {
|
||||
report(mc, false, "Tracy capture started: " + this.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String startCapture(int seconds) {
|
||||
final File candidate = new File(SystemProperties.PROFILE_DIR, AsprofRecorder.fileName("tracy", "tracy", System.currentTimeMillis()));
|
||||
final String error = Tracy.captureStart(candidate.getPath(), seconds);
|
||||
if (error != null) return error;
|
||||
this.file = candidate;
|
||||
this.inFlight = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
public String path() {
|
||||
return this.file == null ? null : this.file.getAbsolutePath();
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onClientTick(TickEvent.ClientTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
|
||||
if (this.captureKey != null && this.captureKey.isPressed()) {
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
if (!this.inFlight) return;
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
final int state = Tracy.captureState();
|
||||
if (state == TracyBackend.CAPTURE_DONE) {
|
||||
report(mc, false, "Tracy capture saved: " + this.path() + sizeSuffix(this.file));
|
||||
this.inFlight = false;
|
||||
} else if (state == TracyBackend.CAPTURE_FAILED) {
|
||||
report(mc, true, "Tracy capture failed: " + Tracy.captureError());
|
||||
this.inFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String sizeSuffix(File file) {
|
||||
if (file == null) return "";
|
||||
final long bytes = file.length();
|
||||
return String.format(Locale.ROOT, " (%.1f MB)", bytes / (1024.0 * 1024.0));
|
||||
}
|
||||
|
||||
private static void report(Minecraft mc, boolean failure, String text) {
|
||||
if (mc.thePlayer != null) {
|
||||
mc.thePlayer.addChatMessage(new ChatComponentText((failure ? EnumChatFormatting.RED : EnumChatFormatting.AQUA) + "[Angelica] " + text));
|
||||
} else if (failure) {
|
||||
LOGGER.warn(text);
|
||||
} else {
|
||||
LOGGER.info(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.loading.DependencyVerifier;
|
||||
import com.gtnewhorizons.angelica.glsm.loading.Lwjgl3ifyExclusions;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.TracyOptions;
|
||||
import com.gtnewhorizons.angelica.loading.fml.compat.CompatHandlers;
|
||||
import com.gtnewhorizons.angelica.lwjgl3.MissingDependencySdl;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
|
||||
@@ -66,6 +67,7 @@ public final class AngelicaClientTweaker implements IFMLLoadingPlugin, IEarlyMix
|
||||
try {
|
||||
// Angelica Config
|
||||
ConfigurationManager.registerConfig(AngelicaConfig.class);
|
||||
TracyOptions.latch(AngelicaConfig.enableTracy, AngelicaConfig.tracyAllowRemote, AngelicaConfig.tracyFineZones, AngelicaConfig.tracyMaxSrcLocs);
|
||||
ConfigurationManager.registerConfig(CompatConfig.class);
|
||||
ConfigurationManager.registerConfig(FontConfig.class);
|
||||
MCPatcherForgeConfig.registerConfig();
|
||||
|
||||
@@ -2,12 +2,12 @@ package com.gtnewhorizons.angelica.mixins;
|
||||
|
||||
import com.gtnewhorizon.gtnhmixins.builders.IMixins;
|
||||
import com.gtnewhorizon.gtnhmixins.builders.MixinBuilder;
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
import com.gtnewhorizons.angelica.api.BlockLightProvider;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.config.CompatConfig;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.CaptureGate;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.TracyOptions;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
|
||||
import jss.notfine.config.MCPatcherForgeConfig;
|
||||
import jss.notfine.config.NotFineConfig;
|
||||
@@ -236,7 +236,7 @@ public enum Mixins implements IMixins {
|
||||
|
||||
ANGELICA_TRACY(new MixinBuilder("Tracy profiler zones from vanilla Profiler sections")
|
||||
.setPhase(Phase.EARLY)
|
||||
.setApplyIf(() -> AngelicaMod.tracyEnabled)
|
||||
.setApplyIf(() -> TracyOptions.enabled() && TracyOptions.backendPresent())
|
||||
.addCommonMixins(
|
||||
"angelica.tracy.MixinProfiler_Tracy"
|
||||
, "angelica.tracy.MixinNetHandlerPlayServer_Tracy"
|
||||
@@ -250,14 +250,14 @@ public enum Mixins implements IMixins {
|
||||
|
||||
ANGELICA_ENABLE_DEBUG(new MixinBuilder()
|
||||
.setPhase(Phase.EARLY)
|
||||
.setApplyIf(() -> AngelicaMod.lwjglDebug)
|
||||
.setApplyIf(() -> SystemProperties.LWJGL_DEBUG)
|
||||
.addClientMixins(
|
||||
"angelica.debug.MixinSplashProgress"
|
||||
)
|
||||
),
|
||||
ANGELICA_DEBUG_MARKERS(new MixinBuilder("RenderDoc/RGP debug groups + object labels")
|
||||
.setPhase(Phase.EARLY)
|
||||
.setApplyIf(() -> AngelicaMod.lwjglDebug || CaptureGate.enabledAtStartup())
|
||||
.setApplyIf(() -> SystemProperties.LWJGL_DEBUG || CaptureGate.enabledAtStartup())
|
||||
.addClientMixins(
|
||||
"angelica.debug.MixinProfiler"
|
||||
, "angelica.debug.MixinTextureManager"
|
||||
@@ -327,7 +327,7 @@ public enum Mixins implements IMixins {
|
||||
// Not compatible with the lwjgl debug callbacks, so disable if that's enabled
|
||||
ARCHAIC_SPLASH(new MixinBuilder()
|
||||
.setPhase(Phase.EARLY)
|
||||
.setApplyIf(() -> AngelicaConfig.showSplashMemoryBar && !AngelicaMod.lwjglDebug)
|
||||
.setApplyIf(() -> AngelicaConfig.showSplashMemoryBar && !SystemProperties.LWJGL_DEBUG)
|
||||
.addClientMixins(
|
||||
"angelica.archaic.MixinSplashProgress$3",
|
||||
"angelica.archaic.AccessorSplashProgress"
|
||||
|
||||
@@ -52,6 +52,7 @@ import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.debug.F3Direction;
|
||||
import com.gtnewhorizons.angelica.debug.flyby.FlybyFallGuard;
|
||||
import com.gtnewhorizons.angelica.debug.flyby.FlybyRunner;
|
||||
import com.gtnewhorizons.angelica.debug.profiling.TracyCaptureNotifier;
|
||||
import com.gtnewhorizons.angelica.debug.FrametimeGraph;
|
||||
import com.gtnewhorizons.angelica.debug.TPSGraph;
|
||||
import com.gtnewhorizons.angelica.dynamiclights.DynamicLights;
|
||||
@@ -197,13 +198,17 @@ public final class ClientProxy extends CommonProxy {
|
||||
}
|
||||
|
||||
// Debug tooling
|
||||
if (SystemProperties.debugTooling()) {
|
||||
if (SystemProperties.debugTooling() || Tracy.ENABLED) {
|
||||
ClientCommandHandler.instance.registerCommand(new AngelicaCommand());
|
||||
|
||||
}
|
||||
if (SystemProperties.debugTooling()) {
|
||||
FMLCommonHandler.instance().bus().register(FlybyRunner.INSTANCE);
|
||||
MinecraftForge.EVENT_BUS.register(FlybyFallGuard.INSTANCE);
|
||||
FlybyRunner.INSTANCE.startFromProperties();
|
||||
}
|
||||
if (Tracy.ENABLED) {
|
||||
TracyCaptureNotifier.INSTANCE.register();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -42,8 +42,8 @@ public final class FpsReducer {
|
||||
private FpsReducer() {}
|
||||
|
||||
public static void init() {
|
||||
forceKey = new KeyBinding("angelica.keybind.fps_reducer_force", 0, "key.categories.misc");
|
||||
disableKey = new KeyBinding("angelica.keybind.fps_reducer_disable", 0, "key.categories.misc");
|
||||
forceKey = new KeyBinding("angelica.keybind.fps_reducer_force", 0, AngelicaMod.KEY_CATEGORY);
|
||||
disableKey = new KeyBinding("angelica.keybind.fps_reducer_disable", 0, AngelicaMod.KEY_CATEGORY);
|
||||
ClientRegistry.registerKeyBinding(forceKey);
|
||||
ClientRegistry.registerKeyBinding(disableKey);
|
||||
FMLCommonHandler.instance().bus().register(INSTANCE);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.zoom;
|
||||
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
import cpw.mods.fml.client.registry.ClientRegistry;
|
||||
import cpw.mods.fml.common.FMLCommonHandler;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
@@ -29,7 +30,7 @@ public class Zoom {
|
||||
private static long zoomLerpLastNano = 0L;
|
||||
|
||||
@Getter
|
||||
private static final KeyBinding zoomKey = new KeyBinding("angelica.keybind.zoom", 0, "key.categories.misc");
|
||||
private static final KeyBinding zoomKey = new KeyBinding("angelica.keybind.zoom", 0, AngelicaMod.KEY_CATEGORY);
|
||||
|
||||
private static boolean zoomEnabled = false;
|
||||
|
||||
|
||||
@@ -3,9 +3,12 @@ package me.jellysquid.mods.sodium.client.gui;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.gtnewhorizons.angelica.client.gui.DynamicLightsOptionPages;
|
||||
import com.gtnewhorizons.angelica.client.gui.ScrollableGuiScreen;
|
||||
import com.gtnewhorizons.angelica.client.gui.TracyOptionPages;
|
||||
import com.gtnewhorizons.angelica.compat.mojang.Drawable;
|
||||
import com.gtnewhorizons.angelica.compat.mojang.Element;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.dynamiclights.DynamicLights;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.TracyOptions;
|
||||
import jss.notfine.gui.GuiCustomMenu;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.Option;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.OptionFlag;
|
||||
@@ -70,6 +73,10 @@ public class SodiumOptionsGUI extends ScrollableGuiScreen {
|
||||
this.pages.add(DynamicLightsOptionPages.dynamicLights());
|
||||
}
|
||||
|
||||
if (SystemProperties.debugTooling() || TracyOptions.backendPresent()) {
|
||||
this.pages.add(TracyOptionPages.tracy());
|
||||
}
|
||||
|
||||
if (Iris.enabled) {
|
||||
shaderPacks = new OptionPage(I18n.format("options.iris.shaderPackSelection"), ImmutableList.of());
|
||||
this.pages.add(shaderPacks);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package me.jellysquid.mods.sodium.client.gui.options;
|
||||
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ActionOption extends ButtonOption {
|
||||
|
||||
private final Supplier<String> label;
|
||||
private final Runnable action;
|
||||
private final BooleanSupplier available;
|
||||
|
||||
public ActionOption(String name, String tooltip, Supplier<String> label, Runnable action, BooleanSupplier available) {
|
||||
super(name, tooltip, 100);
|
||||
this.label = label;
|
||||
this.action = action;
|
||||
this.available = available;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String label() {
|
||||
return this.label.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invoke() {
|
||||
this.action.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return this.available.getAsBoolean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package me.jellysquid.mods.sodium.client.gui.options;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import me.jellysquid.mods.sodium.client.gui.options.control.Control;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.control.ControlElement;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.control.element.ControlElementFactory;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.control.element.SodiumControlElement;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.storage.OptionStorage;
|
||||
import me.jellysquid.mods.sodium.client.util.Dim2i;
|
||||
|
||||
public abstract class ButtonOption implements Option<Void> {
|
||||
|
||||
private static final int LABEL_HOVERED = 0xFF94E4D3;
|
||||
private static final int LABEL_IDLE = 0xFFAAAAAA;
|
||||
|
||||
private static final OptionStorage<Void> NO_STORAGE = new OptionStorage<Void>() {
|
||||
|
||||
@Override
|
||||
public Void getData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {}
|
||||
};
|
||||
|
||||
private final String name;
|
||||
private final String tooltip;
|
||||
private final Control<Void> control;
|
||||
|
||||
protected ButtonOption(String name, String tooltip, int maxWidth) {
|
||||
this.name = name;
|
||||
this.tooltip = tooltip;
|
||||
this.control = new ButtonControl(this, maxWidth);
|
||||
}
|
||||
|
||||
protected abstract String label();
|
||||
|
||||
protected abstract void invoke();
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTooltip() {
|
||||
return this.tooltip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionImpact getImpact() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Control<Void> getControl() {
|
||||
return this.control;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void getValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Void value) {}
|
||||
|
||||
@Override
|
||||
public void reset() {}
|
||||
|
||||
@Override
|
||||
public OptionStorage<?> getStorage() {
|
||||
return NO_STORAGE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasChanged() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyChanges() {}
|
||||
|
||||
@Override
|
||||
public Collection<OptionFlag> getFlags() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
private record ButtonControl(ButtonOption target, int maxWidth) implements Control<Void> {
|
||||
|
||||
@Override
|
||||
public Option<Void> getOption() {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ControlElement<Void> createElement(Dim2i dim, ControlElementFactory factory) {
|
||||
return new ButtonElement(this.target, dim);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxWidth() {
|
||||
return this.maxWidth;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ButtonElement extends SodiumControlElement<Void> {
|
||||
|
||||
private final ButtonOption target;
|
||||
|
||||
ButtonElement(ButtonOption target, Dim2i dim) {
|
||||
super(target, dim);
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(int mouseX, int mouseY, float delta) {
|
||||
super.render(mouseX, mouseY, delta);
|
||||
|
||||
final String label = this.target.label();
|
||||
final int width = this.font.getStringWidth(label);
|
||||
this.drawString(label, this.dim.getLimitX() - width - 6, this.dim.getCenterY() - 4, this.hovered ? LABEL_HOVERED : LABEL_IDLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (!this.target.isAvailable() || button != 0 || !this.dim.containsCursor(mouseX, mouseY)) return false;
|
||||
|
||||
this.playClickSound();
|
||||
this.target.invoke();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,149 +1,30 @@
|
||||
package me.jellysquid.mods.sodium.client.gui.options;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.function.Function;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
|
||||
import me.jellysquid.mods.sodium.client.gui.options.control.Control;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.control.ControlElement;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.control.element.ControlElementFactory;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.control.element.SodiumControlElement;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.storage.OptionStorage;
|
||||
import me.jellysquid.mods.sodium.client.util.Dim2i;
|
||||
|
||||
/** "Option" that opens a sub screen */
|
||||
public class SubScreenOption implements Option<Void> {
|
||||
public class SubScreenOption extends ButtonOption {
|
||||
|
||||
private static final int ARROW_HOVERED = 0xFF94E4D3;
|
||||
private static final int ARROW_IDLE = 0xFFAAAAAA;
|
||||
private static final String ARROW = ">";
|
||||
|
||||
private static final OptionStorage<Void> NO_STORAGE = new OptionStorage<Void>() {
|
||||
|
||||
@Override
|
||||
public Void getData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {}
|
||||
};
|
||||
|
||||
private final String name;
|
||||
private final String tooltip;
|
||||
private final Function<GuiScreen, GuiScreen> destination;
|
||||
private final Control<Void> control = new SubScreenControl(this);
|
||||
|
||||
public SubScreenOption(String name, String tooltip, Function<GuiScreen, GuiScreen> destination) {
|
||||
this.name = name;
|
||||
this.tooltip = tooltip;
|
||||
super(name, tooltip, 30);
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
private void open() {
|
||||
@Override
|
||||
protected String label() {
|
||||
return ARROW;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invoke() {
|
||||
final var mc = Minecraft.getMinecraft();
|
||||
mc.displayGuiScreen(this.destination.apply(mc.currentScreen));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTooltip() {
|
||||
return this.tooltip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionImpact getImpact() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Control<Void> getControl() {
|
||||
return this.control;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void getValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Void value) {}
|
||||
|
||||
@Override
|
||||
public void reset() {}
|
||||
|
||||
@Override
|
||||
public OptionStorage<?> getStorage() {
|
||||
return NO_STORAGE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasChanged() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyChanges() {}
|
||||
|
||||
@Override
|
||||
public Collection<OptionFlag> getFlags() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
private record SubScreenControl(SubScreenOption target) implements Control<Void> {
|
||||
|
||||
@Override
|
||||
public Option<Void> getOption() {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ControlElement<Void> createElement(Dim2i dim, ControlElementFactory factory) {
|
||||
return new SubScreenElement(this.target, dim);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxWidth() {
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SubScreenElement extends SodiumControlElement<Void> {
|
||||
|
||||
private final SubScreenOption target;
|
||||
|
||||
SubScreenElement(SubScreenOption option, Dim2i dim) {
|
||||
super(option, dim);
|
||||
this.target = option;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(int mouseX, int mouseY, float delta) {
|
||||
super.render(mouseX, mouseY, delta);
|
||||
|
||||
final var width = this.font.getStringWidth(ARROW);
|
||||
this.drawString(ARROW, this.dim.getLimitX() - width - 6, this.dim.getCenterY() - 4, this.hovered ? ARROW_HOVERED : ARROW_IDLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (button != 0 || !this.dim.containsCursor(mouseX, mouseY)) return false;
|
||||
|
||||
this.playClickSound();
|
||||
this.target.open();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -39,6 +39,10 @@ public interface ControlValueFormatter {
|
||||
return (v) -> v + "x";
|
||||
}
|
||||
|
||||
static ControlValueFormatter seconds() {
|
||||
return (v) -> v + "s";
|
||||
}
|
||||
|
||||
static ControlValueFormatter quantity(String name) {
|
||||
return (v) -> I18n.format(name, v);
|
||||
}
|
||||
|
||||
@@ -155,9 +155,11 @@ key.category.iris.keybinds=Iris Keybinds
|
||||
iris.keybind.reload=Reload Shaders
|
||||
iris.keybind.toggleShaders=Toggle Shaders
|
||||
iris.keybind.shaderPackSelection=Shaderpack Selection Screen
|
||||
key.categories.angelica=Angelica
|
||||
angelica.keybind.zoom=Zoom
|
||||
angelica.keybind.fps_reducer_force=Force FPS Reducer
|
||||
angelica.keybind.fps_reducer_disable=Toggle FPS Reducer
|
||||
angelica.keybind.tracyCapture=Start/Stop Tracy Capture
|
||||
angelica.fps_reducer.hud.forced=FPS reducer: forced
|
||||
angelica.fps_reducer.hud.disabled=FPS reducer: disabled
|
||||
|
||||
@@ -289,6 +291,31 @@ options.angelica.gpuCullingMode.tooltip=Selects the chunk culling pipeline. CPU
|
||||
options.angelica.gpuCullingMode.cpu_only=CPU Only
|
||||
options.angelica.gpuCullingMode.compute=Compute
|
||||
|
||||
options.angelica.tracy.page=Tracy Profiler
|
||||
options.angelica.tracy.enable=Enable Tracy
|
||||
options.angelica.tracy.enable.tooltip=Enable the Tracy profiler backend. Requires angelica-tracy.jar on the classpath. Takes effect after a game restart.
|
||||
options.angelica.tracy.allowRemote=Allow Remote Connections
|
||||
options.angelica.tracy.allowRemote.tooltip=Accept Tracy connections from other machines instead of localhost only. Takes effect after a game restart.
|
||||
options.angelica.tracy.fineZones=Fine Zones
|
||||
options.angelica.tracy.fineZones.tooltip=Emit Tracy zones for many more call sites. More detail, more overhead. Takes effect after a game restart.
|
||||
options.angelica.tracy.maxSrcLocs=Max Source Locations
|
||||
options.angelica.tracy.maxSrcLocs.tooltip=Maximum distinct Tracy zone and message call sites to preallocate. Takes effect after a game restart.
|
||||
options.angelica.tracy.overriddenBy=Overridden by -D%s
|
||||
options.angelica.tracy.status=Status
|
||||
options.angelica.tracy.status.tooltip=Whether Tracy is currently running and whether it matches the settings above.
|
||||
options.angelica.tracy.status.restart=Restart to apply
|
||||
options.angelica.tracy.status.running=Running
|
||||
options.angelica.tracy.status.runningForced=Running (forced by JVM flag)
|
||||
options.angelica.tracy.status.failed=Failed to start, see log
|
||||
options.angelica.tracy.status.off=Off
|
||||
options.angelica.tracy.captureLength=Capture Length
|
||||
options.angelica.tracy.captureLength.tooltip=Default length of a Tracy capture started from this page or /angelica tracy start. Captures are saved to angelica/profiles and open with Tracy Profiler 0.14.
|
||||
options.angelica.tracy.captureAction=Capture
|
||||
options.angelica.tracy.captureAction.tooltip=Starts or stops a Tracy capture. Saved to angelica/profiles and opened with Tracy Profiler 0.14.
|
||||
options.angelica.tracy.captureAction.start=Start capture
|
||||
options.angelica.tracy.captureAction.stop=Stop capture
|
||||
options.angelica.tracy.captureAction.saving=Saving...
|
||||
|
||||
pack.iris.select.title=Select
|
||||
pack.iris.configure.title=Configure
|
||||
pack.iris.list.label=+ Drag and Drop Shader Packs to add
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.gtnewhorizons.angelica.arch;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaAccess;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class EarlyLoadingBoundaryTest {
|
||||
|
||||
private static final String TRACY = "com.gtnewhorizons.angelica.glsm.profiling.Tracy";
|
||||
|
||||
private static final Pattern GUARDED = Pattern.compile(
|
||||
".*/com/gtnewhorizons/angelica/(loading/[^/]+|mixins/Mixins(\\$[^/]*)?|AngelicaMod(\\$[^/]*)?|config/AngelicaConfig(\\$[^/]*)?)\\.class$");
|
||||
|
||||
private static final JavaClasses classes = new ClassFileImporter()
|
||||
.withImportOption(location -> location.matches(GUARDED))
|
||||
.importPaths("build/classes/java/main");
|
||||
|
||||
@Test
|
||||
void earlyLoadingClassesDoNotAccessTracy() {
|
||||
assertFalse(classes.isEmpty(), "no guarded classes imported");
|
||||
final List<String> violations = new ArrayList<>();
|
||||
for (JavaClass origin : classes) {
|
||||
for (JavaAccess<?> access : origin.getAccessesFromSelf()) {
|
||||
if (access.getTargetOwner().getName().equals(TRACY)) {
|
||||
violations.add(origin.getFullName() + " -> " + access.getTarget().getFullName());
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(violations.isEmpty(), "Coremod-phase code must not touch glsm.profiling.Tracy:\n" + String.join("\n", violations));
|
||||
}
|
||||
}
|
||||
@@ -39,8 +39,8 @@ class AsprofRecorderTest {
|
||||
@Test
|
||||
void fileNameMatchesShape() {
|
||||
final long fixedMillis = 1_700_000_000_000L;
|
||||
final String circuit = AsprofRecorder.fileName("circuit", fixedMillis);
|
||||
final String manual = AsprofRecorder.fileName("manual", fixedMillis);
|
||||
final String circuit = AsprofRecorder.fileName("circuit", "jfr", fixedMillis);
|
||||
final String manual = AsprofRecorder.fileName("manual", "jfr", fixedMillis);
|
||||
assertTrue(circuit.matches("angelica-circuit-\\d{8}-\\d{6}\\.jfr"));
|
||||
assertTrue(manual.matches("angelica-manual-\\d{8}-\\d{6}\\.jfr"));
|
||||
}
|
||||
@@ -48,7 +48,7 @@ class AsprofRecorderTest {
|
||||
@Test
|
||||
void fileNameIsDeterministicForSameMillis() {
|
||||
final long fixedMillis = 1_700_000_000_000L;
|
||||
assertEquals(AsprofRecorder.fileName("circuit", fixedMillis), AsprofRecorder.fileName("circuit", fixedMillis));
|
||||
assertEquals(AsprofRecorder.fileName("circuit", "jfr", fixedMillis), AsprofRecorder.fileName("circuit", "jfr", fixedMillis));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.gtnewhorizons.angelica.tracy;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.lwjgl.system.JNI;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import org.lwjgl.system.SharedLibrary;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static org.lwjgl.system.MemoryStack.stackPush;
|
||||
import static org.lwjgl.system.MemoryUtil.NULL;
|
||||
|
||||
final class TracyCapture {
|
||||
private static final Logger LOGGER = LogManager.getLogger("Tracy");
|
||||
private static final int MEM_PERCENT = 25;
|
||||
private static final String UNAVAILABLE = "capture library unavailable on this platform";
|
||||
|
||||
private boolean loaded;
|
||||
private String loadError;
|
||||
private long capStart;
|
||||
private long capStop;
|
||||
private long capState;
|
||||
private long capElapsedMs;
|
||||
private long capError;
|
||||
|
||||
synchronized String start(String path, int port, int seconds) {
|
||||
final String err = ensureLoaded();
|
||||
if (err != null) return err;
|
||||
final File file = new File(path);
|
||||
final File parent = file.getParentFile();
|
||||
if (parent != null && !parent.mkdirs() && !parent.isDirectory()) {
|
||||
return "cannot create directory " + parent;
|
||||
}
|
||||
try (MemoryStack stack = stackPush()) {
|
||||
final ByteBuffer pathUtf8 = stack.UTF8(path, true);
|
||||
final int result = JNI.invokePI(MemoryUtil.memAddress(pathUtf8), port, seconds, MEM_PERCENT, capStart);
|
||||
if (result == 1) return "a capture is already running";
|
||||
if (result != 0) return "capture start failed (code " + result + ")";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void stop() {
|
||||
if (!loaded) return;
|
||||
JNI.invokeV(capStop);
|
||||
}
|
||||
|
||||
synchronized int state() {
|
||||
return loaded ? JNI.invokeI(capState) : 0;
|
||||
}
|
||||
|
||||
synchronized long elapsedMs() {
|
||||
return loaded ? JNI.invokeJ(capElapsedMs) : 0L;
|
||||
}
|
||||
|
||||
synchronized String error() {
|
||||
if (!loaded) return "";
|
||||
final long ptr = JNI.invokeP(capError);
|
||||
return ptr == NULL ? "" : MemoryUtil.memUTF8(ptr);
|
||||
}
|
||||
|
||||
private String ensureLoaded() {
|
||||
if (loaded) return null;
|
||||
if (loadError != null) return loadError;
|
||||
final SharedLibrary lib = TracyNativeLoader.load("AngelicaTracyCapture");
|
||||
if (lib == null) {
|
||||
loadError = UNAVAILABLE;
|
||||
return loadError;
|
||||
}
|
||||
try {
|
||||
capStart = req(lib, "ang_cap_start");
|
||||
capStop = req(lib, "ang_cap_stop");
|
||||
capState = req(lib, "ang_cap_state");
|
||||
capElapsedMs = req(lib, "ang_cap_elapsed_ms");
|
||||
capError = req(lib, "ang_cap_error");
|
||||
} catch (IllegalStateException e) {
|
||||
LOGGER.warn("Tracy: {}", e.getMessage());
|
||||
loadError = UNAVAILABLE;
|
||||
return loadError;
|
||||
}
|
||||
loaded = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long req(SharedLibrary lib, String name) {
|
||||
final long address = lib.getFunctionAddress(name);
|
||||
if (address == NULL) throw new IllegalStateException("missing symbol " + name + " in " + lib.getPath());
|
||||
return address;
|
||||
}
|
||||
}
|
||||
+59
-3
@@ -1,9 +1,9 @@
|
||||
package com.gtnewhorizons.angelica.tracy;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.TracyBackend;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.TracyOptions;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.ZoneStack;
|
||||
import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@@ -41,6 +41,8 @@ public final class TracyClientBackend implements TracyBackend {
|
||||
private SrcLocInterner interner;
|
||||
private long emptyString;
|
||||
private int ctxSize;
|
||||
private int port;
|
||||
private final TracyCapture capture = new TracyCapture();
|
||||
int zoneCtxSize() {
|
||||
return ctxSize;
|
||||
}
|
||||
@@ -52,7 +54,7 @@ public final class TracyClientBackend implements TracyBackend {
|
||||
LOGGER.warn("Tracy: 64-bit JVM required");
|
||||
return false;
|
||||
}
|
||||
lib = TracyNativeLoader.load();
|
||||
lib = TracyNativeLoader.load("TracyClient");
|
||||
if (lib == null) return false;
|
||||
sym = TracyLibrary.resolve(lib);
|
||||
if (sym == null) return false;
|
||||
@@ -66,7 +68,27 @@ public final class TracyClientBackend implements TracyBackend {
|
||||
emptyString = nmemAllocChecked(1);
|
||||
memPutByte(emptyString, (byte) 0);
|
||||
|
||||
interner = new SrcLocInterner(this::allocSrcLoc, SystemProperties.TRACY_MAX_SRC_LOCS);
|
||||
interner = new SrcLocInterner(this::allocSrcLoc, TracyOptions.maxSrcLocs());
|
||||
|
||||
final String envPort = System.getenv("TRACY_PORT");
|
||||
if (envPort != null) {
|
||||
port = TracyPorts.parsePort(envPort);
|
||||
if (port <= 0) {
|
||||
LOGGER.warn("Tracy: TRACY_PORT='{}' is not a valid port (1-65535); in-game capture disabled", envPort);
|
||||
}
|
||||
} else {
|
||||
port = TracyPorts.pick();
|
||||
if (port > 0) {
|
||||
if (!setEnv("TRACY_PORT", String.valueOf(port))) port = 0;
|
||||
} else {
|
||||
LOGGER.warn("Tracy: no free port in 8086-8105; in-game capture disabled, set the TRACY_PORT environment variable to pin one");
|
||||
}
|
||||
}
|
||||
if (!TracyOptions.allowRemote() && !(setEnv("TRACY_ONLY_LOCALHOST", "1") && setEnv("TRACY_ONLY_IPV4", "1"))) {
|
||||
LOGGER.warn("Tracy: cannot restrict the listener to localhost; not starting (enable Allow remote connections on the Tracy settings page to allow remote)");
|
||||
return false;
|
||||
}
|
||||
LOGGER.info("Tracy: using port {} ({})", port, TracyOptions.allowRemote() ? "remote allowed" : "localhost only");
|
||||
|
||||
JNI.invokeV(sym.startupProfiler);
|
||||
if (JNI.invokeI(sym.profilerStarted) == 0) {
|
||||
@@ -82,6 +104,14 @@ public final class TracyClientBackend implements TracyBackend {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setEnv(String name, String value) {
|
||||
try (MemoryStack stack = stackPush()) {
|
||||
final ByteBuffer nameUtf8 = stack.UTF8(name, true);
|
||||
final ByteBuffer valueUtf8 = stack.UTF8(value, true);
|
||||
return JNI.invokePPI(memAddress(nameUtf8), memAddress(valueUtf8), sym.setEnv) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
private long allocSrcLoc(String name, int color) {
|
||||
final ByteBuffer nameUtf8 = MemoryUtil.memUTF8(name);
|
||||
final long srcLoc = nmemAllocChecked(32);
|
||||
@@ -242,6 +272,32 @@ public final class TracyClientBackend implements TracyBackend {
|
||||
return JNI.invokeI(sym.connected) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String captureStart(String path, int seconds) {
|
||||
if (port <= 0) return "no free Tracy port in 8086-8105; set the TRACY_PORT environment variable";
|
||||
return capture.start(path, port, seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void captureStop() {
|
||||
capture.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int captureState() {
|
||||
return capture.state();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long captureElapsedMs() {
|
||||
return capture.elapsedMs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String captureError() {
|
||||
return capture.error();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
JNI.invokeV(sym.shutdownProfiler);
|
||||
|
||||
@@ -36,6 +36,7 @@ final class TracyLibrary {
|
||||
final long gpuTimeSync;
|
||||
final long gpuNewContext;
|
||||
final long gpuContextName;
|
||||
final long setEnv;
|
||||
|
||||
static TracyLibrary resolve(SharedLibrary lib) {
|
||||
try {
|
||||
@@ -72,6 +73,7 @@ final class TracyLibrary {
|
||||
gpuTimeSync = req(lib, "___tracy_emit_gpu_time_sync");
|
||||
gpuNewContext = req(lib, "___tracy_emit_gpu_new_context");
|
||||
gpuContextName = req(lib, "___tracy_emit_gpu_context_name");
|
||||
setEnv = req(lib, "ang_set_env");
|
||||
}
|
||||
|
||||
private static long req(SharedLibrary lib, String name) {
|
||||
|
||||
@@ -33,7 +33,7 @@ final class TracyNativeLoader {
|
||||
|
||||
private TracyNativeLoader() {}
|
||||
|
||||
static SharedLibrary load() {
|
||||
static SharedLibrary load(String baseName) {
|
||||
final OS os = OS.detect(System.getProperty("os.name", "").toLowerCase());
|
||||
final String arch = System.getProperty("os.arch", "").toLowerCase();
|
||||
final boolean arm64 = arch.contains("aarch64") || arch.contains("arm64");
|
||||
@@ -43,9 +43,9 @@ final class TracyNativeLoader {
|
||||
}
|
||||
final String platform = os.name().toLowerCase() + (arm64 ? "-arm64" : "-x64");
|
||||
final String libName = switch (os) {
|
||||
case WINDOWS -> "TracyClient.dll";
|
||||
case MACOS -> "libTracyClient.dylib";
|
||||
case LINUX -> "libTracyClient.so";
|
||||
case WINDOWS -> baseName + ".dll";
|
||||
case MACOS -> "lib" + baseName + ".dylib";
|
||||
case LINUX -> "lib" + baseName + ".so";
|
||||
};
|
||||
final String resource = "/natives/tracy/" + TRACY_VERSION + "/" + platform + "/" + libName;
|
||||
|
||||
@@ -63,11 +63,11 @@ final class TracyNativeLoader {
|
||||
|
||||
final String ext = libName.substring(libName.lastIndexOf('.') + 1);
|
||||
final File dir = new File(SystemProperties.TRACY_DIR);
|
||||
final File target = new File(dir, "libTracyClient-" + TRACY_VERSION + "-" + platform + "." + ext);
|
||||
final File target = new File(dir, "lib" + baseName + "-" + TRACY_VERSION + "-" + platform + "." + ext);
|
||||
try {
|
||||
if (!target.isFile() || target.length() != lib.length || !Arrays.equals(Files.readAllBytes(target.toPath()), lib)) {
|
||||
if (!dir.isDirectory() && !dir.mkdirs()) throw new IOException("cannot create " + dir);
|
||||
final File tmp = File.createTempFile("libTracyClient", "." + ext, dir);
|
||||
final File tmp = File.createTempFile("lib" + baseName, "." + ext, dir);
|
||||
Files.write(tmp.toPath(), lib);
|
||||
Files.move(tmp.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.gtnewhorizons.angelica.tracy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
|
||||
final class TracyPorts {
|
||||
private static final int RANGE_START = 8086;
|
||||
private static final int RANGE_END = 8105;
|
||||
|
||||
private TracyPorts() {}
|
||||
|
||||
static int pick() {
|
||||
for (int port = RANGE_START; port <= RANGE_END; port++) {
|
||||
try (ServerSocket socket = new ServerSocket(port)) {
|
||||
return port;
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parsePort(String value) {
|
||||
try {
|
||||
final int port = Integer.parseInt(value.trim());
|
||||
return port >= 1 && port <= 65535 ? port : 0;
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
# 13.3: TracyServer uses floating-point std::to_chars, unavailable in older libc++
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 13.3)
|
||||
project(angelica-tracy-capture LANGUAGES C CXX)
|
||||
|
||||
if(NOT DEFINED TRACY_SRC_DIR)
|
||||
message(FATAL_ERROR "TRACY_SRC_DIR must be set to a Tracy source checkout")
|
||||
endif()
|
||||
|
||||
set(NO_ISA_EXTENSIONS ON)
|
||||
set(NO_LTO ON)
|
||||
set(NO_STATISTICS ON)
|
||||
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_C_VISIBILITY_PRESET hidden)
|
||||
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
|
||||
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
|
||||
|
||||
include(${TRACY_SRC_DIR}/cmake/config.cmake)
|
||||
include(${TRACY_SRC_DIR}/cmake/vendor.cmake)
|
||||
include(${TRACY_SRC_DIR}/cmake/server.cmake)
|
||||
|
||||
add_library(AngelicaTracyCapture SHARED angelica_tracy_capture.cpp)
|
||||
set_target_properties(AngelicaTracyCapture PROPERTIES
|
||||
OUTPUT_NAME "AngelicaTracyCapture"
|
||||
CXX_VISIBILITY_PRESET hidden
|
||||
VISIBILITY_INLINES_HIDDEN ON
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
target_link_libraries(AngelicaTracyCapture PRIVATE TracyServer)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
target_link_options(AngelicaTracyCapture PRIVATE -static-libstdc++ -static-libgcc -Wl,--exclude-libs,ALL)
|
||||
endif()
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
target_link_options(AngelicaTracyCapture PRIVATE
|
||||
-Wl,-exported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/angelica_tracy_capture.exports
|
||||
)
|
||||
endif()
|
||||
@@ -0,0 +1,208 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cinttypes>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "TracyFileWrite.hpp"
|
||||
#include "TracySysUtil.hpp"
|
||||
#include "TracyWorker.hpp"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define ANG_CAP_API __declspec(dllexport)
|
||||
#else
|
||||
#define ANG_CAP_API __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
enum CapState : int {
|
||||
STATE_IDLE = 0,
|
||||
STATE_CONNECTING = 1,
|
||||
STATE_RECORDING = 2,
|
||||
STATE_SAVING = 3,
|
||||
STATE_DONE = 4,
|
||||
STATE_FAILED = 5,
|
||||
};
|
||||
|
||||
std::atomic<int> g_state{ STATE_IDLE };
|
||||
std::atomic<bool> g_stopRequested{ false };
|
||||
std::atomic<int64_t> g_recordStartNs{ 0 };
|
||||
std::atomic<int64_t> g_frozenElapsedMs{ -1 };
|
||||
std::mutex g_mutex;
|
||||
std::thread g_thread;
|
||||
std::string g_error;
|
||||
std::string g_errorSnapshot;
|
||||
|
||||
struct ThreadJoiner {
|
||||
~ThreadJoiner() {
|
||||
g_stopRequested.store(true, std::memory_order_release);
|
||||
if (g_thread.joinable()) {
|
||||
g_thread.join();
|
||||
}
|
||||
}
|
||||
};
|
||||
ThreadJoiner g_threadJoiner;
|
||||
|
||||
uint64_t CurrentPid() {
|
||||
#if defined(_WIN32)
|
||||
return (uint64_t)GetCurrentProcessId();
|
||||
#else
|
||||
return (uint64_t)getpid();
|
||||
#endif
|
||||
}
|
||||
|
||||
void SetError(const std::string& msg) {
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
g_error = msg;
|
||||
}
|
||||
|
||||
void RunCapture(std::string path, int port, int seconds, int memPercent) {
|
||||
try {
|
||||
int64_t memLimit = -1;
|
||||
if (memPercent > 0) {
|
||||
const int64_t clamped = std::clamp<int64_t>(memPercent, 1, 999);
|
||||
memLimit = clamped * (int64_t)tracy::GetPhysicalMemorySize() / 100;
|
||||
}
|
||||
|
||||
tracy::Worker worker("127.0.0.1", (uint16_t)port, memLimit);
|
||||
|
||||
const auto waitStart = std::chrono::steady_clock::now();
|
||||
while (!worker.HasData()) {
|
||||
if (g_stopRequested.load(std::memory_order_acquire)) {
|
||||
SetError("capture cancelled before the client connected");
|
||||
g_state.store(STATE_FAILED, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
const auto handshake = worker.GetHandshakeStatus();
|
||||
if (handshake == tracy::HandshakeProtocolMismatch) {
|
||||
SetError("client uses an incompatible protocol version");
|
||||
g_state.store(STATE_FAILED, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
if (handshake == tracy::HandshakeNotAvailable) {
|
||||
SetError("client not available for connection");
|
||||
g_state.store(STATE_FAILED, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
if (handshake == tracy::HandshakeDropped) {
|
||||
SetError("client disconnected during handshake");
|
||||
g_state.store(STATE_FAILED, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
if (std::chrono::steady_clock::now() - waitStart >= std::chrono::seconds(5)) {
|
||||
SetError("timed out connecting to the Tracy client; its port may be taken, set -Dangelica.tracy.port");
|
||||
g_state.store(STATE_FAILED, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
const uint64_t pid = worker.GetPid();
|
||||
if (pid != CurrentPid()) {
|
||||
char buf[96];
|
||||
snprintf(buf, sizeof(buf), "attached to another process (pid %" PRIu64 ")", pid);
|
||||
SetError(buf);
|
||||
worker.Disconnect();
|
||||
g_state.store(STATE_FAILED, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
g_state.store(STATE_RECORDING, std::memory_order_release);
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
g_recordStartNs.store(t0.time_since_epoch().count(), std::memory_order_release);
|
||||
|
||||
while (worker.IsConnected()) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const bool timeUp = seconds > 0 && std::chrono::duration_cast<std::chrono::seconds>(now - t0).count() >= seconds;
|
||||
if (g_stopRequested.load(std::memory_order_acquire) || timeUp) {
|
||||
worker.Disconnect();
|
||||
while (worker.IsConnected()) std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - t0).count();
|
||||
g_frozenElapsedMs.store(elapsedMs, std::memory_order_release);
|
||||
|
||||
g_state.store(STATE_SAVING, std::memory_order_release);
|
||||
std::unique_ptr<tracy::FileWrite> f(tracy::FileWrite::Open(path.c_str(), tracy::FileCompression::Zstd, 3, 4));
|
||||
if (!f) {
|
||||
SetError("cannot open output file");
|
||||
g_state.store(STATE_FAILED, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
worker.Write(*f, false);
|
||||
f->Finish();
|
||||
g_state.store(STATE_DONE, std::memory_order_release);
|
||||
} catch (const std::exception& e) {
|
||||
SetError(e.what());
|
||||
g_state.store(STATE_FAILED, std::memory_order_release);
|
||||
} catch (...) {
|
||||
SetError("unknown capture error");
|
||||
g_state.store(STATE_FAILED, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
ANG_CAP_API int ang_cap_start(const char* path, int port, int seconds, int memPercent) {
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
|
||||
const int state = g_state.load(std::memory_order_acquire);
|
||||
if (state == STATE_CONNECTING || state == STATE_RECORDING || state == STATE_SAVING) {
|
||||
return 1;
|
||||
}
|
||||
if (g_thread.joinable()) {
|
||||
g_thread.join();
|
||||
}
|
||||
|
||||
g_stopRequested.store(false, std::memory_order_release);
|
||||
g_recordStartNs.store(0, std::memory_order_release);
|
||||
g_frozenElapsedMs.store(-1, std::memory_order_release);
|
||||
g_error.clear();
|
||||
g_state.store(STATE_CONNECTING, std::memory_order_release);
|
||||
|
||||
g_thread = std::thread(RunCapture, std::string(path ? path : ""), port, seconds, memPercent);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ANG_CAP_API void ang_cap_stop(void) {
|
||||
g_stopRequested.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
ANG_CAP_API int ang_cap_state(void) {
|
||||
return g_state.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
ANG_CAP_API int64_t ang_cap_elapsed_ms(void) {
|
||||
const int64_t frozen = g_frozenElapsedMs.load(std::memory_order_acquire);
|
||||
if (frozen >= 0) return frozen;
|
||||
if (g_state.load(std::memory_order_acquire) != STATE_RECORDING) return 0;
|
||||
const int64_t startNs = g_recordStartNs.load(std::memory_order_acquire);
|
||||
if (startNs == 0) return 0;
|
||||
const int64_t nowNs = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
return (nowNs - startNs) / 1000000;
|
||||
}
|
||||
|
||||
ANG_CAP_API const char* ang_cap_error(void) {
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
g_errorSnapshot = g_error;
|
||||
return g_errorSnapshot.c_str();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
_ang_cap_start
|
||||
_ang_cap_stop
|
||||
_ang_cap_state
|
||||
_ang_cap_elapsed_ms
|
||||
_ang_cap_error
|
||||
@@ -1,6 +1,11 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#include "tracy/TracyC.h"
|
||||
#include "client/TracyProfiler.hpp"
|
||||
@@ -50,4 +55,12 @@ TRACY_API void ang_section_setup(uint32_t category, const char* txt) {
|
||||
tracy::Profiler::SectionSetup((uint16_t)category, "%s", txt);
|
||||
}
|
||||
|
||||
TRACY_API int ang_set_env(const char* name, const char* value) {
|
||||
#if defined(_WIN32)
|
||||
return SetEnvironmentVariableA(name, value) ? 0 : 1;
|
||||
#else
|
||||
return setenv(name, value, 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
+82
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.tracy;
|
||||
import com.gtnewhorizon.gtnhlib.reflect.Fields;
|
||||
import com.gtnewhorizon.gtnhlib.reflect.Fields.LookupType;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.TracyBackend;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -11,14 +12,17 @@ import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@EnabledIf("nativePresent")
|
||||
@@ -42,6 +46,17 @@ class TracyBindingSmokeTest {
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean captureLibPresent() {
|
||||
for (String platform : new String[] {"windows-x64", "linux-x64", "macos-x64", "macos-arm64"}) {
|
||||
for (String lib : new String[] {"AngelicaTracyCapture.dll", "libAngelicaTracyCapture.so", "libAngelicaTracyCapture.dylib"}) {
|
||||
if (TracyBindingSmokeTest.class.getResource("/natives/tracy/" + TracyTags.TRACY_VERSION + "/" + platform + "/" + lib) != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static synchronized TracyClientBackend backend() throws Exception {
|
||||
if (backend == null) {
|
||||
Fields.ofClass(SystemProperties.class).getField(LookupType.DECLARED, "TRACY_DIR", String.class).setValue(null, extractDir.toAbsolutePath().toString());
|
||||
@@ -171,6 +186,73 @@ class TracyBindingSmokeTest {
|
||||
|
||||
@Test
|
||||
@Order(7)
|
||||
@EnabledIf("captureLibPresent")
|
||||
void captureLifecycle(@TempDir Path tmp) throws Exception {
|
||||
final TracyClientBackend b = backend();
|
||||
final long srcLoc = b.internSrcLoc("capture-smoke", 0);
|
||||
final Path file = tmp.resolve("t.tracy");
|
||||
|
||||
final Thread emitter = startEmitter(b, srcLoc, 1500L);
|
||||
final String error = b.captureStart(file.toAbsolutePath().toString(), 1);
|
||||
assertNull(error, "captureStart failed: " + error);
|
||||
|
||||
final int state = pollUntilDone(b);
|
||||
emitter.join(5_000);
|
||||
|
||||
assertEquals(TracyBackend.CAPTURE_DONE, state, "capture did not finish, last state " + state + ": " + b.captureError());
|
||||
assertTrue(Files.isRegularFile(file), "capture file missing: " + file);
|
||||
assertTrue(Files.size(file) > 0, "capture file empty: " + file);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(8)
|
||||
@EnabledIf("captureLibPresent")
|
||||
void captureReconnect(@TempDir Path tmp) throws Exception {
|
||||
final TracyClientBackend b = backend();
|
||||
final long srcLoc = b.internSrcLoc("capture-smoke-2", 0);
|
||||
final Path file = tmp.resolve("t2.tracy");
|
||||
|
||||
final Thread emitter = startEmitter(b, srcLoc, 1500L);
|
||||
final String error = b.captureStart(file.toAbsolutePath().toString(), 0);
|
||||
assertNull(error, "captureStart failed: " + error);
|
||||
|
||||
Thread.sleep(500);
|
||||
b.captureStop();
|
||||
|
||||
final int state = pollUntilDone(b);
|
||||
emitter.join(5_000);
|
||||
|
||||
assertEquals(TracyBackend.CAPTURE_DONE, state, "capture did not finish, last state " + state + ": " + b.captureError());
|
||||
assertTrue(Files.isRegularFile(file), "capture file missing: " + file);
|
||||
assertTrue(Files.size(file) > 0, "capture file empty: " + file);
|
||||
}
|
||||
|
||||
private static Thread startEmitter(TracyClientBackend b, long srcLoc, long durationMs) {
|
||||
final Thread t = new Thread(() -> {
|
||||
b.setCurrentThreadName("capture-emitter");
|
||||
final long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(durationMs);
|
||||
while (System.nanoTime() < deadline) {
|
||||
final long ctx = b.beginZone(srcLoc);
|
||||
b.endZone(ctx);
|
||||
b.frameMark();
|
||||
}
|
||||
}, "capture-emitter");
|
||||
t.start();
|
||||
return t;
|
||||
}
|
||||
|
||||
private static int pollUntilDone(TracyClientBackend b) throws InterruptedException {
|
||||
final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(15);
|
||||
int state = b.captureState();
|
||||
while (state != TracyBackend.CAPTURE_DONE && state != TracyBackend.CAPTURE_FAILED && System.nanoTime() < deadline) {
|
||||
Thread.sleep(50);
|
||||
state = b.captureState();
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(9)
|
||||
void shutdownFlushes() throws Exception {
|
||||
backend().shutdown();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.gtnewhorizons.angelica.tracy;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class TracyPortsTest {
|
||||
|
||||
@Test
|
||||
void autoPickSkipsAnOccupiedPort() throws IOException {
|
||||
for (int port = 8086; port <= 8105; port++) {
|
||||
try (ServerSocket held = new ServerSocket(port)) {
|
||||
final int picked = TracyPorts.pick();
|
||||
assertNotEquals(port, picked);
|
||||
assertTrue(picked > 0);
|
||||
return;
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsePortAcceptsTheValidRange() {
|
||||
assertEquals(1, TracyPorts.parsePort("1"));
|
||||
assertEquals(65535, TracyPorts.parsePort("65535"));
|
||||
assertEquals(9000, TracyPorts.parsePort("9000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsePortRejectsOutOfRangeAndGarbage() {
|
||||
assertEquals(0, TracyPorts.parsePort("0"));
|
||||
assertEquals(0, TracyPorts.parsePort("65536"));
|
||||
assertEquals(0, TracyPorts.parsePort("-1"));
|
||||
assertEquals(0, TracyPorts.parsePort("not a port"));
|
||||
assertEquals(0, TracyPorts.parsePort(""));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user