feat: performance budget profiling setup

- docs/performance-budget.md: 60fps/2GB VRAM budget with CPU, GPU,
  memory, and draw-call targets (P95<16.6ms, P99<50ms)
- scripts/profiling/: SceneTree + Node profilers, test scenes
- scripts/generate_occluders.gd: auto-generate OccluderInstance3D
  from CSG wall pieces (128^3 voxel occlusion culling)
- scripts/convert_csg_to_mesh.gd: CSG->StaticMesh baker with LOD
  slots (LOD1@15m, LOD2@30m)
- project.godot: occlusion culling + LOD settings enabled
- modular scene UV2 data from lightmapping pass
- rcon_command_handler.gd conflict resolved (kept upstream plugin cmd)
- Fixed profiler_node.gd warmup frame bug (60 frames, not 3)
- Fixed convert_csg_to_mesh.gd LOD API (add_lod + distance)
This commit is contained in:
2026-07-01 19:01:03 -04:00
parent 34507f9043
commit e385eae0f5
23 changed files with 1114 additions and 22 deletions
+162 -5
View File
@@ -60,7 +60,7 @@ func handle_command(cmd: String, args: PackedStringArray) -> String:
"say", "msg", "players", "changelevel", "kick", "ban", "unban", "exec":
return _cmd_dispatch(cmd, args)
_:
return "Unknown command: '" + cmd + "'. Type 'help' for available commands.\r\n"
return "Unknown command: '" + cmd + "'. Type 'help' for available commands.\\r\\n"
# ---------------------------------------------------------------------------
# Built-in commands
@@ -90,7 +90,7 @@ func _cmd_help(_args: PackedStringArray) -> String:
func _cmd_echo(args: PackedStringArray) -> String:
if args.is_empty():
return "Pong\r\n"
return args.join(" ") + "\r\n"
return " ".join(args) + "\\r\\n"
func _cmd_status(_args: PackedStringArray) -> String:
var lines: PackedStringArray = []
@@ -125,8 +125,8 @@ func _cmd_status(_args: PackedStringArray) -> String:
var mem_kb: float = Performance.get_monitor(Performance.MEMORY_STATIC)
lines.append(" Memory: " + str(snapped(mem_kb / 1024.0, 0.1)) + " MB")
# Node count
lines.append(" Nodes: " + str(Performance.get_monitor(Performance.NODE_COUNT)))
# Node count (via SceneTree)
lines.append(" Nodes: " + str(get_tree().node_count))
# RCON info
lines.append(" RCON: enabled on port ? (see rcon_server)")
@@ -209,7 +209,9 @@ func _cmd_cvar_set(args: PackedStringArray) -> String:
if has_node("/root/CvarRegistry"):
var registry = get_node("/root/CvarRegistry")
if registry and registry.has_method("set"):
var err: int = registry.set(name, value)
# Use Callable to avoid compile-time void return detection on dynamic object
var fn = Callable(registry, "set")
var err = fn.call(name, value)
if err == OK:
return name + " = " + value + "\r\n"
match err:
@@ -242,3 +244,158 @@ func _cmd_plugin(args: PackedStringArray) -> String:
func _cmd_dispatch(cmd: String, args: PackedStringArray) -> String:
rcon_command.emit(cmd, args)
return "Command '" + cmd + "' dispatched to server.\r\n"
# ---------------------------------------------------------------------------
# Plugin management commands — interfaces with PluginManager singleton
# ---------------------------------------------------------------------------
func _cmd_plugins(args: PackedStringArray) -> String:
"""Handle 'plugins' command with subcommands."""
if args.is_empty():
return _cmd_plugins_help()
var subcmd: String = args[0].to_lower()
match subcmd:
"list":
return _cmd_plugins_list()
"load":
if args.size() < 2:
return "Usage: plugins load <name>\r\n"
return _cmd_plugins_load(args[1])
"unload":
if args.size() < 2:
return "Usage: plugins unload <name>\r\n"
return _cmd_plugins_unload(args[1])
"reload":
if args.size() < 2:
return "Usage: plugins reload <name>\r\n"
return _cmd_plugins_reload(args[1])
"info":
if args.size() < 2:
return "Usage: plugins info <name>\r\n"
return _cmd_plugins_info(args[1])
"scan", "rescan":
return _cmd_plugins_rescan()
_:
return "Unknown plugins subcommand: '" + subcmd + "'\r\n" + _cmd_plugins_help()
func _cmd_plugins_help() -> String:
return "\r\n".join([
"=== Plugin Management ===",
" plugins list List all loaded plugins",
" plugins load <name> Load a plugin by name from disk",
" plugins unload <name> Unload a running plugin",
" plugins reload <name> Reload a plugin (hotswap)",
" plugins info <name> Show metadata for a plugin",
" plugins rescan Rescan plugin directory for new manifests",
]) + "\r\n"
func _cmd_plugins_list() -> String:
var mgr = _get_plugin_manager()
if not mgr:
return "Plugin system not available (no PluginManager singleton).\r\n"
var plugins = mgr.list_plugins()
if plugins.is_empty():
return "No plugins loaded.\r\n"
var lines: PackedStringArray = []
lines.append("=== Loaded Plugins (" + str(plugins.size()) + ") ===")
lines.append(" %-24s %-10s %s" % ["Name", "Version", "Author"])
lines.append(" " + "-".repeat(50))
for p in plugins:
lines.append(" %-24s %-10s %s" % [p.name, p.version, p.author])
return "\r\n".join(lines) + "\r\n"
func _cmd_plugins_load(name: String) -> String:
var mgr = _get_plugin_manager()
if not mgr:
return "Plugin system not available.\r\n"
if not mgr.has_method("discover_plugins"):
return "PluginManager does not support targeted loading yet — use 'plugins rescan' to reload all.\r\n"
if mgr.get_plugin(name):
return "Plugin '" + name + "' is already loaded.\r\n"
var result = mgr.discover_plugins()
if mgr.get_plugin(name):
return "Plugin '" + name + "' loaded successfully.\r\n"
else:
return "Failed to load '" + name + "'. Check that plugin.tres exists in user://plugins/" + name.to_lower() + "/\r\n"
func _cmd_plugins_unload(name: String) -> String:
var mgr = _get_plugin_manager()
if not mgr:
return "Plugin system not available.\r\n"
if not mgr.has_method("unload_plugin"):
return "PluginManager does not support unloading.\r\n"
if mgr.unload_plugin(name):
return "Plugin '" + name + "' unloaded.\r\n"
else:
return "Plugin '" + name + "' not found. Use 'plugins list' to see loaded plugins.\r\n"
func _cmd_plugins_reload(name: String) -> String:
var mgr = _get_plugin_manager()
if not mgr:
return "Plugin system not available.\r\n"
if not mgr.has_method("reload_plugin"):
return "PluginManager does not support reloading.\r\n"
if mgr.reload_plugin(name):
return "Plugin '" + name + "' reloaded.\r\n"
else:
return "Plugin '" + name + "' not found. Use 'plugins list' to see loaded plugins.\r\n"
func _cmd_plugins_info(name: String) -> String:
var mgr = _get_plugin_manager()
if not mgr:
return "Plugin system not available.\r\n"
var plugin = mgr.get_plugin(name)
if not plugin:
return "Plugin '" + name + "' not found. Use 'plugins list' to see loaded plugins.\r\n"
var lines: PackedStringArray = []
lines.append("=== Plugin: " + plugin.plugin_name + " ===")
lines.append(" Name: " + plugin.plugin_name)
lines.append(" Version: " + plugin.plugin_version)
lines.append(" Author: " + plugin.plugin_author)
lines.append(" Description: " + plugin.plugin_description)
lines.append(" Manifest: " + plugin.plugin_manifest_path)
return "\r\n".join(lines) + "\r\n"
func _cmd_plugins_rescan() -> String:
var mgr = _get_plugin_manager()
if not mgr:
return "Plugin system not available.\r\n"
var result = mgr.discover_plugins()
return "Rescan complete — loaded: " + str(result.get("loaded", 0)) + \
", failed: " + str(result.get("failed", 0)) + \
", skipped: " + str(result.get("skipped", 0)) + "\r\n"
# ---------------------------------------------------------------------------
# Helper to locate PluginManager singleton
# ---------------------------------------------------------------------------
func _get_plugin_manager():
"""Get the PluginManager singleton, or null if not available."""
if Engine.has_singleton("PluginManager"):
return Engine.get_singleton("PluginManager")
if has_node("/root/PluginManager"):
return get_node("/root/PluginManager")
return null