From 86603f015e2f1d4623fe243cc07cabca6eff20a1 Mon Sep 17 00:00:00 2001 From: Bryce Date: Tue, 10 Mar 2026 13:08:26 -0700 Subject: [PATCH] Test vram --- .beads/dolt-monitor.pid.lock | 0 AGENTS.md | 4 + project.godot | 1 + res:/measure_vram.gd | 101 ++++++++++++++++ tools/estimate_vram.py | 223 +++++++++++++++++++++++++++++++++++ 5 files changed, 329 insertions(+) delete mode 100644 .beads/dolt-monitor.pid.lock create mode 100644 res:/measure_vram.gd create mode 100644 tools/estimate_vram.py diff --git a/.beads/dolt-monitor.pid.lock b/.beads/dolt-monitor.pid.lock deleted file mode 100644 index e69de29..0000000 diff --git a/AGENTS.md b/AGENTS.md index e5689c3..9f3d815 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -295,6 +295,10 @@ func _on_exit_interacted() -> void: $target_scene.default_script(self) ``` +## Branching strategy + +Do all work in a branch. Squash the branch before merging into master. + ## Issue Tracking with bd (beads) diff --git a/project.godot b/project.godot index 547041b..613d6a7 100644 --- a/project.godot +++ b/project.godot @@ -23,6 +23,7 @@ config/icon="res://icon.png" ActionState="*res://ActionState.gd" CameraTransition="*res://camera_transition.tscn" +VRAMMonitor="*res://measure_vram.gd" [display] diff --git a/res:/measure_vram.gd b/res:/measure_vram.gd new file mode 100644 index 0000000..a4829c2 --- /dev/null +++ b/res:/measure_vram.gd @@ -0,0 +1,101 @@ +extends Node + +## VRAM/Texture Memory Measurement Tool +## Add this as an autoload to measure memory usage at startup + +var measurements := {} +var start_time := 0.0 + +func _ready(): + start_time = Time.get_ticks_msec() + + # Initial measurement (before anything loads) + _record_measurement("initial") + + # Wait for scene tree to be ready + await get_tree().process_frame + _record_measurement("after_tree_ready") + + # Wait for main scene to fully load + await get_tree().process_frame + _record_measurement("after_main_scene_loaded") + + # Give textures time to upload to GPU + await get_tree().process_frame + await get_tree().process_frame + _record_measurement("after_gpu_upload") + + # Print final report + await get_tree().process_frame + _print_report() + +func _record_measurement(label: String): + var elapsed_ms = Time.get_ticks_msec() - start_time + var tex_mem = Performance.get_monitor(Performance.VIEWER_TEXTURE_MEMORY) + var obj_count = Performance.get_monitor(Performance.OBJECT_COUNT) + var node_count = Performance.get_monitor(Performance.OBJECT_NODE_COUNT) + + measurements[label] = { + "elapsed_ms": elapsed_ms, + "texture_memory_bytes": tex_mem, + "texture_memory_mb": tex_mem / (1024.0 * 1024.0), + "object_count": obj_count, + "node_count": node_count + } + + print("[%.2f ms] %s: VRAM=%.2f MB, Objects=%d, Nodes=%d" % [ + elapsed_ms / 1000.0, + label, + tex_mem / (1024.0 * 1024.0), + obj_count, + node_count + ]) + +func _print_report(): + print("\n" + "=" * 60) + print("VRAM USAGE REPORT") + print("=" * 60) + + var total_elapsed = Time.get_ticks_msec() - start_time + + for label in measurements: + var m = measurements[label] + print("\n[%s]" % label) + print(" Elapsed: %.3f s" % (m.elapsed_ms / 1000.0)) + print(" Texture Memory: %.2f MB (%.2f KB)" % [m.texture_memory_mb, m.texture_memory_bytes / 1024.0]) + print(" Object Count: %d" % m.object_count) + print(" Node Count: %d" % m.node_count) + + print("\n" + "=" * 60) + print("TOTAL STARTUP TIME: %.3f s" % (total_elapsed / 1000.0)) + print("=" * 60) + + # Save to file if we have write access + var file_path = "user://vram_measurement_%s.txt" % Time.get_datetime_string_from_system(false, true).replace(":", "-") + var file = FileAccess.open(file_path, FileAccess.WRITE) + if file: + file.store_string("VRAM Measurement Report\n") + file.store_string("Generated: %s\n\n" % Time.get_datetime_string_from_system()) + for label in measurements: + var m = measurements[label] + file.store_string("[%s]\n" % label) + file.store_string(" Elapsed: %.3f s\n" % (m.elapsed_ms / 1000.0)) + file.store_string(" Texture Memory: %.2f MB\n" % m.texture_memory_mb) + file.store_string(" Object Count: %d\n" % m.object_count) + file.store_string(" Node Count: %d\n\n" % m.node_count) + file.close() + print("\nReport saved to: %s" % file_path) + +## Call this manually at any point to get current VRAM usage +func get_current_vram_mb() -> float: + return Performance.get_monitor(Performance.VIEWER_TEXTURE_MEMORY) / (1024.0 * 1024.0) + +## Get detailed breakdown +func get_memory_stats() -> Dictionary: + return { + "texture_memory_mb": get_current_vram_mb(), + "object_count": Performance.get_monitor(Performance.OBJECT_COUNT), + "node_count": Performance.get_monitor(Performance.OBJECT_NODE_COUNT), + "physics_objects_2d": Performance.get_monitor(Performance.PHYSICS_2D_OBJECT_COUNT), + "physics_objects_3d": Performance.get_monitor(Performance.PHYSICS_3D_OBJECT_COUNT), + } diff --git a/tools/estimate_vram.py b/tools/estimate_vram.py new file mode 100644 index 0000000..672421d --- /dev/null +++ b/tools/estimate_vram.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +""" +Estimate VRAM usage by analyzing Godot's imported texture files. +This works without running the game. +""" + +import os +import re +import json +from pathlib import Path +from collections import defaultdict + +def parse_import_file(import_path): + """Parse a .import file to get texture dimensions and format.""" + dimensions = None + format_type = "unknown" + compressed = False + + try: + with open(import_path, 'r') as f: + content = f.read() + + # Look for dimension info in the import file + # Godot stores this in the remap section + dim_match = re.search(r'width=(\d+)', content) + height_match = re.search(r'height=(\d+)', content) + + if dim_match and height_match: + dimensions = (int(dim_match.group(1)), int(height_match.group(2))) + + # Check for compression + if 'ctex' in import_path or 'compress' in content.lower(): + compressed = True + format_type = "compressed" + elif 'texture' in content.lower(): + format_type = "rgba" + + except Exception as e: + pass + + return dimensions, format_type, compressed + +def estimate_vram_size(width, height, format_type="rgba", compressed=False): + """Estimate VRAM size in bytes.""" + if compressed: + # BC7/DXT5 compression is roughly 4:1 + return (width * height * 4) // 4 + else: + # RGBA = 4 bytes per pixel + return width * height * 4 + +def scan_ego_resources(project_root): + """Scan ego directory for all texture resources.""" + ego_path = Path(project_root) / "ego" + + if not ego_path.exists(): + print("ERROR: ego/ directory not found") + return {} + + textures = [] + + # Find all .import files + for import_file in ego_path.rglob("*.import"): + # Get the original PNG path + png_path = import_file.with_suffix('.png') + + if png_path.exists(): + # Get file size + file_size = png_path.stat().st_size + + # Try to get dimensions from the import file + dimensions, format_type, compressed = parse_import_file(import_file) + + # Estimate VRAM + if dimensions: + vram_bytes = estimate_vram_size(*dimensions, format_type, compressed) + else: + # Fallback: estimate from file size (compressed textures are smaller) + vram_bytes = file_size * 2 # Rough estimate + + textures.append({ + 'path': str(png_path.relative_to(project_root)), + 'file_size': file_size, + 'dimensions': dimensions, + 'vram_estimate': vram_bytes, + 'compressed': compressed + }) + + return textures + +def analyze_scene_resources(project_root, scene_path): + """Analyze a scene file to count external texture resources.""" + scene_file = Path(project_root) / scene_path + + if not scene_file.exists(): + return [] + + textures = [] + + with open(scene_file, 'r') as f: + content = f.read() + + # Find all ext_resource entries for Texture2D + pattern = r'\[ext_resource type="Texture2D".*?path="([^"]+)"' + matches = re.findall(pattern, content) + + for texture_path in matches: + full_path = Path(project_root) / texture_path + if full_path.exists(): + textures.append(str(texture_path)) + + return textures + +def main(): + project_root = Path("/home/noti/dev/ai-game-2") + + print("=" * 70) + print("VRAM USAGE ESTIMATOR (Static Analysis)") + print("=" * 70) + print() + + # Analyze Ego resources + print("Scanning ego/ directory...") + ego_textures = scan_ego_resources(project_root) + + if not ego_textures: + print("No textures found in ego/") + return + + # Group by animation set + animation_sets = defaultdict(list) + for tex in ego_textures: + if 'minstrel-v3' in tex['path']: + animation_sets['minstrel-v3'].append(tex) + elif 'minstrel-v5' in tex['path']: + animation_sets['minstrel-v5'].append(tex) + elif 'rosella' in tex['path']: + animation_sets['rosella'].append(tex) + elif 'portrait' in tex['path']: + animation_sets['portrait'].append(tex) + elif 'mask' in tex['path']: + animation_sets['masks'].append(tex) + else: + animation_sets['other'].append(tex) + + print(f"\nFound {len(ego_textures)} texture files in ego/") + print() + + # Calculate totals + total_disk = 0 + total_vram_compressed = 0 + total_vram_uncompressed = 0 + + print("Breakdown by Animation Set:") + print("-" * 70) + + for set_name, textures in sorted(animation_sets.items()): + if not textures: + continue + + disk_sum = sum(t['file_size'] for t in textures) + vram_compressed_sum = sum(t['vram_estimate'] for t in textures) + vram_uncompressed_sum = sum(t['vram_estimate'] * 4 for t in textures) + + total_disk += disk_sum + total_vram_compressed += vram_compressed_sum + total_vram_uncompressed += vram_uncompressed_sum + + print(f"\n{set_name.upper()}:") + print(f" Files: {len(textures)}") + print(f" Disk Size: {disk_sum / (1024*1024):.2f} MB") + print(f" VRAM (compressed): {vram_compressed_sum / (1024*1024):.2f} MB") + print(f" VRAM (uncompressed): {vram_uncompressed_sum / (1024*1024):.2f} MB") + + print() + print("=" * 70) + print("TOTALS:") + print("=" * 70) + print(f"Total Files: {len(ego_textures)}") + print(f"Total Disk Size: {total_disk / (1024*1024):.2f} MB") + print(f"Total VRAM (compressed): {total_vram_compressed / (1024*1024):.2f} MB") + print(f"Total VRAM (uncompressed): {total_vram_uncompressed / (1024*1024):.2f} MB") + print() + + # Analyze main scene + print("Analyzing Game.tscn...") + game_textures = analyze_scene_resources(project_root, "Game.tscn") + print(f" External textures referenced: {len(game_textures)}") + + # Analyze Ego scene + print("Analyzing Ego.tscn...") + ego_scene_textures = analyze_scene_resources(project_root, "Ego.tscn") + print(f" External textures referenced: {len(ego_scene_textures)}") + + # Count unique minstrel-v3 frames + minstrel_v3_count = len([t for t in ego_textures if 'minstrel-v3' in t['path']]) + print(f"\nMinstrel-v3 frames loaded at startup: {minstrel_v3_count}") + + # Estimate startup VRAM + minstrel_v3_vram = sum(t['vram_estimate'] for t in ego_textures if 'minstrel-v3' in t['path']) + other_vram = total_vram_compressed - minstrel_v3_vram + + print() + print("=" * 70) + print("STARTUP VRAM ESTIMATE (Compressed):") + print("=" * 70) + print(f"Ego (minstrel-v3 only): {minstrel_v3_vram / (1024*1024):.2f} MB") + print(f"Other ego resources: {other_vram / (1024*1024):.2f} MB") + print(f"Estimated room textures: ~5.00 MB (estimated)") + print(f"Estimated UI/cursors: ~1.00 MB (estimated)") + print(f"----------------------------------------") + print(f"TOTAL STARTUP VRAM: ~{(minstrel_v3_vram + other_vram) / (1024*1024) + 6:.2f} MB") + print() + print("UNCOMPRESSED estimate: ~{(minstrel_v3_vram + other_vram) * 4 / (1024*1024) + 24:.2f} MB") + print() + print("Note: These are estimates. Actual VRAM may vary based on:") + print(" - Godot's texture compression settings") + print(" - GPU driver optimizations") + print(" - Texture mipmap generation") + print("=" * 70) + +if __name__ == "__main__": + main()