platform: Cx.perf_monitor — per-frame ring (240) of paint-to-paint gap +
per-channel CPU us. Built-in channels: event dispatch (outermost, minus
app-attributed time), script exec, GC, pass encode, nextDrawable wait.
Apps register custom channels: cx.perf_monitor.channel("physics", rgb).
Off until enabled; hooks in event dispatch, macos repaint, metal draw_pass.
widgets: PerfGraph — corner-pinned live panel (DrawVector strips): frame-gap
bars colored against 120/60Hz budgets with guide lines, stacked per-channel
CPU, legend with averages. Self-positions bottom-right (DrawVector geometry
+ deferred turtle alignment don't mix — no aligning parent).
gamemaker: PerfGraph hovers the game pane (F3 toggles), engine feeds script
+ physics channels (incl. hot-reload evals); engine text overlay moved to
F4; per-phase engine window kept for ag perf / AIGAME_PERF=1; new 'ag perf'
harness verb + template guidance (template CLAUDE.md force-added: runtime
resource, blanket CLAUDE.md gitignore had kept it untracked).
Measured on my-game-5: engine frame CPU ~0.3ms; the hiccup is frame pacing —
the 8ms NSTimer paint clock beats against the 120Hz display, the drawable
pool drifts full and nextDrawable blocks the main thread in a ~25-frame
sawtooth (avg 2-3.4ms, spikes 20-30ms). The graph shows it as red ramps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
95 lines
3.3 KiB
Bash
95 lines
3.3 KiB
Bash
#!/usr/bin/env bash
|
|
# Agent harness for the in-process Game Maker engine. The game runs INSIDE the
|
|
# Game Maker app; these verbs talk to it through the .agent/ directory.
|
|
#
|
|
# tools/ag peek 4 screenshots of the LIVE game + entity state -> .agent/sheet.png
|
|
# tools/ag test [frames] [tape] restart + replay an input tape -> .agent/sheet.png + probe.txt
|
|
# tools/ag errors the last eval/runtime error (empty output = your edit is live)
|
|
# tools/ag logs tail of the game log (eval reports + game.log lines)
|
|
# tools/ag perf per-phase frame profile of the live game (~2s window)
|
|
set -uo pipefail
|
|
|
|
PROJ="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
AGENT_DIR="$PROJ/.agent"
|
|
mkdir -p "$AGENT_DIR"
|
|
|
|
die() { echo "ag: $*" >&2; exit 1; }
|
|
|
|
wait_for_file() { # <path> <seconds>
|
|
local deadline=$(( SECONDS + $2 ))
|
|
while [[ ! -f "$1" ]]; do
|
|
(( SECONDS > deadline )) && return 1
|
|
sleep 0.2
|
|
done
|
|
return 0
|
|
}
|
|
|
|
cmd_peek() {
|
|
rm -rf "$AGENT_DIR/live"
|
|
: >"$AGENT_DIR/peek_request"
|
|
wait_for_file "$AGENT_DIR/live/done" 15 || die "peek timed out — is the Game Maker app running this game?"
|
|
# The last PNG lands slightly after done; give the GPU readback a moment.
|
|
wait_for_file "$AGENT_DIR/live/f0003.png" 5 || true
|
|
sleep 0.3
|
|
python3 "$PROJ/tools/sheet.py" "$AGENT_DIR/live" "$AGENT_DIR/sheet.png" 4 4 480 || die "sheet failed"
|
|
echo "--- state ---"
|
|
cat "$AGENT_DIR/live/state.txt" 2>/dev/null
|
|
cmd_errors
|
|
}
|
|
|
|
cmd_test() {
|
|
local frames="${1:-120}" tape="${2:-}"
|
|
rm -f "$AGENT_DIR/test_done" "$AGENT_DIR/probe.txt"
|
|
rm -rf "$AGENT_DIR/cap"
|
|
printf '{"frames":%d,"tape":"%s","every":%d}' \
|
|
"$frames" "$tape" "$(( frames / 16 + 1 ))" >"$AGENT_DIR/test_request"
|
|
# 60 ticks/second plus capture latency.
|
|
local budget=$(( frames / 30 + 20 ))
|
|
wait_for_file "$AGENT_DIR/test_done" "$budget" || die "test timed out — is the Game Maker app running this game?"
|
|
sleep 0.5
|
|
python3 "$PROJ/tools/sheet.py" "$AGENT_DIR/cap" "$AGENT_DIR/sheet.png" 8 4 320 || die "sheet failed"
|
|
echo "--- probe ---"
|
|
cat "$AGENT_DIR/probe.txt" 2>/dev/null | head -40
|
|
cmd_errors
|
|
}
|
|
|
|
cmd_errors() {
|
|
if [[ -s "$AGENT_DIR/last_error.txt" ]]; then
|
|
echo "--- errors ---"
|
|
cat "$AGENT_DIR/last_error.txt"
|
|
else
|
|
# Warnings (unknown option keys etc.) don't fail the eval so they never
|
|
# reach last_error.txt — count them since the last eval, so "no errors"
|
|
# can't hide a misspelled option.
|
|
local warnings
|
|
warnings=$(awk '/ eval #/{n=0; next} /\(ignored\)/{n++} END{print n+0}' \
|
|
"$AGENT_DIR/game.log" 2>/dev/null || echo 0)
|
|
if [[ "${warnings:-0}" -gt 0 ]]; then
|
|
echo "--- no errors ($warnings warnings, see ag logs) ---"
|
|
else
|
|
echo "--- no errors ---"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
cmd_logs() {
|
|
[[ -f "$AGENT_DIR/game.log" ]] || { echo "no log yet"; return 0; }
|
|
tail -40 "$AGENT_DIR/game.log"
|
|
}
|
|
|
|
cmd_perf() {
|
|
rm -f "$AGENT_DIR/perf.txt"
|
|
: >"$AGENT_DIR/perf_request"
|
|
# The engine answers with the next COMPLETED ~2s profiler window.
|
|
wait_for_file "$AGENT_DIR/perf.txt" 8 || die "perf timed out — is the Game Maker app running this game?"
|
|
cat "$AGENT_DIR/perf.txt"
|
|
}
|
|
|
|
case "${1:-}" in
|
|
peek) cmd_peek ;;
|
|
test) shift; cmd_test "$@" ;;
|
|
errors) cmd_errors ;;
|
|
logs) cmd_logs ;;
|
|
perf) cmd_perf ;;
|
|
*) sed -n '2,10p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' ;;
|
|
esac
|