diff --git a/Editor/Core/SceneMemoryReport.cs b/Editor/Core/SceneMemoryReport.cs new file mode 100644 index 0000000..81ab27e --- /dev/null +++ b/Editor/Core/SceneMemoryReport.cs @@ -0,0 +1,183 @@ +using System; +using System.IO; +using UnityEngine; + +namespace SplashEdit.EditorCode +{ + /// + /// Memory analysis report for a single exported scene. + /// All values are in bytes unless noted otherwise. + /// + [Serializable] + public class SceneMemoryReport + { + public string sceneName; + + // ─── Main RAM ─── + public long splashpackFileSize; // Total file on disc + public long splashpackLiveSize; // Bytes kept in RAM at runtime (before bulk data freed) + public int triangleCount; + public int gameObjectCount; + + // ─── VRAM (1024 x 512 x 2 = 1,048,576 bytes) ─── + public long framebufferSize; // 2 x W x H x 2 + public long textureAtlasSize; // Sum of atlas pixel data + public long clutSize; // Sum of CLUT entries x 2 + public long fontVramSize; // Custom font textures + public int atlasCount; + public int clutCount; + + // ─── SPU RAM (512KB, 0x1010 reserved) ─── + public long audioDataSize; + public int audioClipCount; + + // ─── CD Storage ─── + public long loaderPackSize; + + // ─── Constants ─── + public const long TOTAL_RAM = 2 * 1024 * 1024; + public const long KERNEL_RESERVED = 0x10000; // 64KB kernel area + public const long USABLE_RAM = TOTAL_RAM - KERNEL_RESERVED; + public const long TOTAL_VRAM = 1024 * 512 * 2; // 1MB + public const long TOTAL_SPU = 512 * 1024; + public const long SPU_RESERVED = 0x1010; + public const long USABLE_SPU = TOTAL_SPU - SPU_RESERVED; + + // Fixed runtime overhead from C++ (renderer.hh constants) + public const long BUMP_ALLOC_TOTAL = 2L * 8096 * 24; // ~380KB + public const long OT_TOTAL = 2L * 16384 * 4; // ~128KB + public const long VIS_REFS = 4096 * 4; // 16KB + public const long STACK_ESTIMATE = 32 * 1024; // 32KB + public const long LUA_OVERHEAD = 16 * 1024; // 16KB approximate + public const long SYSTEM_FONT_VRAM = 4 * 1024; // ~4KB + + public long FixedOverhead => BUMP_ALLOC_TOTAL + OT_TOTAL + VIS_REFS + STACK_ESTIMATE + LUA_OVERHEAD; + + /// RAM used by scene data (live portion of splashpack). + public long SceneRamUsage => splashpackLiveSize > 0 ? splashpackLiveSize : splashpackFileSize; + + /// Total estimated RAM: fixed overhead + scene data. Does NOT include code/BSS. + public long TotalRamUsage => FixedOverhead + SceneRamUsage; + + public long TotalVramUsed => framebufferSize + textureAtlasSize + clutSize + fontVramSize + SYSTEM_FONT_VRAM; + public long TotalSpuUsed => audioDataSize; + public long TotalDiscSize => splashpackFileSize + loaderPackSize; + + public float RamPercent => Mathf.Clamp01((float)TotalRamUsage / USABLE_RAM) * 100f; + public float VramPercent => Mathf.Clamp01((float)TotalVramUsed / TOTAL_VRAM) * 100f; + public float SpuPercent => USABLE_SPU > 0 ? Mathf.Clamp01((float)TotalSpuUsed / USABLE_SPU) * 100f : 0f; + + public long RamFree => USABLE_RAM - TotalRamUsage; + public long VramFree => TOTAL_VRAM - TotalVramUsed; + public long SpuFree => USABLE_SPU - TotalSpuUsed; + } + + /// + /// Builds a SceneMemoryReport by reading the exported splashpack binary header + /// and the scene's VRAM/audio data. + /// + public static class SceneMemoryAnalyzer + { + /// + /// Analyze an exported scene. Call after ExportToPath(). + /// + /// Display name for the scene. + /// Path to the exported .splashpack file. + /// Path to the loading screen file (may be null). + /// Texture atlases from the export pipeline. + /// Array of ADPCM byte sizes per audio clip. + /// Custom font descriptors. + public static SceneMemoryReport Analyze( + string sceneName, + string splashpackPath, + string loaderPackPath, + SplashEdit.RuntimeCode.TextureAtlas[] atlases, + long[] audioExportSizes, + SplashEdit.RuntimeCode.PSXFontData[] fonts, + int triangleCount = 0) + { + var r = new SceneMemoryReport { sceneName = sceneName }; + + // ── File sizes ── + if (File.Exists(splashpackPath)) + r.splashpackFileSize = new FileInfo(splashpackPath).Length; + if (!string.IsNullOrEmpty(loaderPackPath) && File.Exists(loaderPackPath)) + r.loaderPackSize = new FileInfo(loaderPackPath).Length; + + r.triangleCount = triangleCount; + + // ── Parse splashpack header for counts and pixelDataOffset ── + if (File.Exists(splashpackPath)) + { + try { ReadHeader(splashpackPath, r); } + catch (Exception e) { Debug.LogWarning($"Memory report: failed to read header: {e.Message}"); } + } + + // ── Framebuffers ── + int fbW = SplashSettings.ResolutionWidth; + int fbH = SplashSettings.ResolutionHeight; + int fbCount = SplashSettings.DualBuffering ? 2 : 1; + r.framebufferSize = fbW * fbH * 2L * fbCount; + + // ── VRAM: Texture atlases + CLUTs ── + if (atlases != null) + { + r.atlasCount = atlases.Length; + foreach (var atlas in atlases) + { + r.textureAtlasSize += atlas.Width * SplashEdit.RuntimeCode.TextureAtlas.Height * 2L; + foreach (var tex in atlas.ContainedTextures) + { + if (tex.ColorPalette != null) + { + r.clutCount++; + r.clutSize += tex.ColorPalette.Count * 2L; + } + } + } + } + + // ── VRAM: Custom fonts ── + if (fonts != null) + { + foreach (var font in fonts) + { + if (font.TextureHeight > 0) + r.fontVramSize += 64L * font.TextureHeight * 2; // 4bpp = 64 hwords wide + } + } + + // ── SPU: Audio ── + if (audioExportSizes != null) + { + r.audioClipCount = audioExportSizes.Length; + foreach (long sz in audioExportSizes) + r.audioDataSize += sz; + } + + return r; + } + + private static void ReadHeader(string path, SceneMemoryReport r) + { + using (var reader = new BinaryReader(File.OpenRead(path))) + { + if (reader.BaseStream.Length < 104) return; + + // Magic + version (4 bytes) + reader.ReadBytes(4); + + // luaFileCount(2) + gameObjectCount(2) + textureAtlasCount(2) + clutCount(2) + reader.ReadUInt16(); // luaFileCount + r.gameObjectCount = reader.ReadUInt16(); + reader.ReadUInt16(); // textureAtlasCount + reader.ReadUInt16(); // clutCount + + // Skip to pixelDataOffset at byte 100 + reader.BaseStream.Seek(100, SeekOrigin.Begin); + uint pixelDataOffset = reader.ReadUInt32(); + r.splashpackLiveSize = pixelDataOffset > 0 ? pixelDataOffset : r.splashpackFileSize; + } + } + } +} diff --git a/Editor/Core/SceneMemoryReport.cs.meta b/Editor/Core/SceneMemoryReport.cs.meta new file mode 100644 index 0000000..51ba2f8 --- /dev/null +++ b/Editor/Core/SceneMemoryReport.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f68ba273eb88c3b4796e43f40b226c71 \ No newline at end of file diff --git a/Editor/Core/SplashControlPanel.cs b/Editor/Core/SplashControlPanel.cs index 2c7ddfc..a5977f0 100644 --- a/Editor/Core/SplashControlPanel.cs +++ b/Editor/Core/SplashControlPanel.cs @@ -42,6 +42,10 @@ namespace SplashEdit.EditorCode // ───── Scene List ───── private List _sceneList = new List(); + // ───── Memory Reports ───── + private List _memoryReports = new List(); + private bool _showMemoryReport = true; + // ───── Toolchain Cache ───── private bool _hasMIPS; private bool _hasMake; @@ -585,6 +589,11 @@ namespace SplashEdit.EditorCode // Clean Build toggle SplashSettings.CleanBuild = EditorGUILayout.Toggle("Clean Build", SplashSettings.CleanBuild); + // Memory Overlay toggle + SplashSettings.MemoryOverlay = EditorGUILayout.Toggle( + new GUIContent("Memory Overlay", "Show heap/RAM usage bar at top-right during gameplay"), + SplashSettings.MemoryOverlay); + // Serial port (only for Real Hardware) if (SplashSettings.Target == BuildTarget.RealHardware) { @@ -680,6 +689,145 @@ namespace SplashEdit.EditorCode EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); + + // Memory report (shown after export) + if (_memoryReports.Count > 0) + { + EditorGUILayout.Space(8); + DrawMemoryReports(); + } + } + + // ═══════════════════════════════════════════════════════════════ + // Memory Reports + // ═══════════════════════════════════════════════════════════════ + + private void DrawMemoryReports() + { + _showMemoryReport = DrawSectionFoldout("Memory Report", _showMemoryReport); + if (!_showMemoryReport) return; + + foreach (var report in _memoryReports) + { + EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle); + + GUILayout.Label($"Scene: {report.sceneName}", PSXEditorStyles.SectionHeader); + EditorGUILayout.Space(4); + + // Main RAM bar + DrawMemoryBar("Main RAM", + report.TotalRamUsage, SceneMemoryReport.USABLE_RAM, + report.RamPercent, + new Color(0.3f, 0.6f, 1f), + $"Scene: {FormatBytes(report.SceneRamUsage)} | " + + $"Fixed: {FormatBytes(report.FixedOverhead)} | " + + $"Free: {FormatBytes(report.RamFree)}"); + + EditorGUILayout.Space(4); + + // VRAM bar + DrawMemoryBar("VRAM", + report.TotalVramUsed, SceneMemoryReport.TOTAL_VRAM, + report.VramPercent, + new Color(0.9f, 0.5f, 0.2f), + $"FB: {FormatBytes(report.framebufferSize)} | " + + $"Tex: {FormatBytes(report.textureAtlasSize)} | " + + $"CLUT: {FormatBytes(report.clutSize)} | " + + $"Free: {FormatBytes(report.VramFree)}"); + + EditorGUILayout.Space(4); + + // SPU RAM bar + DrawMemoryBar("SPU RAM", + report.TotalSpuUsed, SceneMemoryReport.USABLE_SPU, + report.SpuPercent, + new Color(0.6f, 0.3f, 0.9f), + report.audioClipCount > 0 + ? $"{report.audioClipCount} clips | {FormatBytes(report.audioDataSize)} | Free: {FormatBytes(report.SpuFree)}" + : "No audio clips"); + + EditorGUILayout.Space(4); + + // CD Storage (no bar, just info) + EditorGUILayout.BeginHorizontal(); + GUILayout.Label("CD Storage:", EditorStyles.miniLabel, GUILayout.Width(70)); + GUILayout.Label( + $"Scene: {FormatBytes(report.splashpackFileSize)}" + + (report.loaderPackSize > 0 ? $" | Loader: {FormatBytes(report.loaderPackSize)}" : "") + + $" | Total: {FormatBytes(report.TotalDiscSize)}", + EditorStyles.miniLabel); + EditorGUILayout.EndHorizontal(); + + // Summary stats + PSXEditorStyles.DrawSeparator(4, 4); + EditorGUILayout.LabelField( + $"{report.gameObjectCount} objects | " + + $"{report.triangleCount} tris | " + + $"{report.atlasCount} atlases | " + + $"{report.clutCount} CLUTs", + PSXEditorStyles.RichLabel); + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(4); + } + } + + private void DrawMemoryBar(string label, long used, long total, float percent, Color barColor, string details) + { + // Label row + EditorGUILayout.BeginHorizontal(); + GUILayout.Label(label, EditorStyles.boldLabel, GUILayout.Width(70)); + GUILayout.Label($"{FormatBytes(used)} / {FormatBytes(total)} ({percent:F1}%)", EditorStyles.miniLabel); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + // Progress bar + Rect barRect = GUILayoutUtility.GetRect(0, 16, GUILayout.ExpandWidth(true)); + + // Background + EditorGUI.DrawRect(barRect, new Color(0.15f, 0.15f, 0.17f)); + + // Fill + float fillFraction = Mathf.Clamp01((float)used / total); + Rect fillRect = new Rect(barRect.x, barRect.y, barRect.width * fillFraction, barRect.height); + + // Color shifts toward red when over 80% + Color fillColor = barColor; + if (percent > 90f) + fillColor = Color.Lerp(PSXEditorStyles.Warning, PSXEditorStyles.Error, (percent - 90f) / 10f); + else if (percent > 80f) + fillColor = Color.Lerp(barColor, PSXEditorStyles.Warning, (percent - 80f) / 10f); + EditorGUI.DrawRect(fillRect, fillColor); + + // Border + DrawRectOutline(barRect, new Color(0.3f, 0.3f, 0.35f)); + + // Percent text overlay + var style = new GUIStyle(EditorStyles.miniLabel) + { + alignment = TextAnchor.MiddleCenter, + normal = { textColor = Color.white } + }; + GUI.Label(barRect, $"{percent:F1}%", style); + + // Details row + GUILayout.Label(details, EditorStyles.miniLabel); + } + + private static void DrawRectOutline(Rect rect, Color color) + { + EditorGUI.DrawRect(new Rect(rect.x, rect.y, rect.width, 1), color); + EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 1, rect.width, 1), color); + EditorGUI.DrawRect(new Rect(rect.x, rect.y, 1, rect.height), color); + EditorGUI.DrawRect(new Rect(rect.xMax - 1, rect.y, 1, rect.height), color); + } + + private static string FormatBytes(long bytes) + { + if (bytes < 0) return "N/A"; + if (bytes < 1024) return $"{bytes} B"; + if (bytes < 1024 * 1024) return $"{bytes / 1024f:F1} KB"; + return $"{bytes / (1024f * 1024f):F2} MB"; } // ═══════════════════════════════════════════════════════════════ @@ -808,6 +956,7 @@ namespace SplashEdit.EditorCode { SplashBuildPaths.EnsureDirectories(); _loaderPackCache = new Dictionary(); + _memoryReports.Clear(); // Save current scene string currentScenePath = SceneManager.GetActiveScene().path; @@ -841,15 +990,34 @@ namespace SplashEdit.EditorCode // Export to the build directory string outputPath = SplashBuildPaths.GetSceneSplashpackPath(i, scene.name); + string loaderPath = null; exporter.ExportToPath(outputPath); Log($"Exported '{scene.name}' → {Path.GetFileName(outputPath)}", LogType.Log); // Export loading screen if assigned if (exporter.LoadingScreenPrefab != null) { - string loaderPath = SplashBuildPaths.GetSceneLoaderPackPath(i, scene.name); + loaderPath = SplashBuildPaths.GetSceneLoaderPackPath(i, scene.name); ExportLoaderPack(exporter.LoadingScreenPrefab, loaderPath, i, scene.name); } + + // Generate memory report for this scene + try + { + var report = SceneMemoryAnalyzer.Analyze( + scene.name, + outputPath, + loaderPath, + exporter.LastExportAtlases, + exporter.LastExportAudioSizes, + exporter.LastExportFonts, + exporter.LastExportTriangleCount); + _memoryReports.Add(report); + } + catch (Exception reportEx) + { + Log($"Memory report for '{scene.name}' failed: {reportEx.Message}", LogType.Warning); + } } catch (Exception ex) { @@ -1017,6 +1185,9 @@ namespace SplashEdit.EditorCode if (SplashSettings.Target == BuildTarget.ISO) buildArg += " LOADER=cdrom"; + if (SplashSettings.MemoryOverlay) + buildArg += " MEMOVERLAY=1"; + int jobCount = Math.Max(1, SystemInfo.processorCount - 1); string cleanPrefix = SplashSettings.CleanBuild ? "make clean && " : ""; string makeCmd = $"{cleanPrefix}make all -j{jobCount} {buildArg}".Trim(); diff --git a/Editor/Core/SplashSettings.cs b/Editor/Core/SplashSettings.cs index 8ecf1c3..821d6d9 100644 --- a/Editor/Core/SplashSettings.cs +++ b/Editor/Core/SplashSettings.cs @@ -124,6 +124,18 @@ namespace SplashEdit.EditorCode set => EditorPrefs.SetBool(Prefix + "CleanBuild", value); } + // --- Memory Overlay --- + /// + /// When enabled, compiles the runtime with a heap/RAM usage progress bar + /// and text overlay at the top-right corner of the screen. + /// Passes MEMOVERLAY=1 to the native Makefile. + /// + public static bool MemoryOverlay + { + get => EditorPrefs.GetBool(Prefix + "MemoryOverlay", false); + set => EditorPrefs.SetBool(Prefix + "MemoryOverlay", value); + } + // --- Export settings --- public static float DefaultGTEScaling { diff --git a/Runtime/PSXSceneExporter.cs b/Runtime/PSXSceneExporter.cs index 2245d30..cc3e676 100644 --- a/Runtime/PSXSceneExporter.cs +++ b/Runtime/PSXSceneExporter.cs @@ -54,11 +54,32 @@ namespace SplashEdit.RuntimeCode private PSXObjectExporter[] _exporters; private TextureAtlas[] _atlases; - + // Component arrays private PSXInteractable[] _interactables; private PSXAudioClip[] _audioSources; private PSXTriggerBox[] _triggerBoxes; + + // ── Post-export data for memory analysis ── + /// Texture atlases from the last export (null before first export). + public TextureAtlas[] LastExportAtlases => _atlases; + /// Custom font data from the last export. + public PSXFontData[] LastExportFonts => _fonts; + /// Audio clip ADPCM sizes from the last export. + public long[] LastExportAudioSizes => _lastAudioSizes; + private long[] _lastAudioSizes; + /// Total triangle count from the last export. + public int LastExportTriangleCount + { + get + { + if (_exporters == null) return 0; + int count = 0; + foreach (var exp in _exporters) + if (exp.Mesh != null) count += exp.Mesh.Triangles.Count; + return count; + } + } // Phase 3+4: World collision and nav regions private PSXCollisionExporter _collisionExporter; @@ -271,6 +292,18 @@ namespace SplashEdit.RuntimeCode audioExports = list.ToArray(); } + // Cache audio sizes for memory analysis + if (audioExports != null) + { + _lastAudioSizes = new long[audioExports.Length]; + for (int i = 0; i < audioExports.Length; i++) + _lastAudioSizes[i] = audioExports[i].adpcmData != null ? audioExports[i].adpcmData.Length : 0; + } + else + { + _lastAudioSizes = null; + } + var scene = new PSXSceneWriter.SceneData { exporters = _exporters, diff --git a/Runtime/SceneMemoryAnalyzer.cs b/Runtime/SceneMemoryAnalyzer.cs new file mode 100644 index 0000000..4532b9b --- /dev/null +++ b/Runtime/SceneMemoryAnalyzer.cs @@ -0,0 +1,255 @@ +using System.Text; +using UnityEngine; + +namespace SplashEdit.RuntimeCode +{ + /// + /// Computes a SceneMemoryReport from scene export data. + /// All sizes match PSXSceneWriter's binary layout exactly. + /// + public static class SceneMemoryAnalyzer + { + // Per-triangle binary size in splashpack (matches PSXSceneWriter.Write mesh section) + // 3 vertices * 6 bytes + normal 6 bytes + 3 colors * 4 bytes + 3 UVs * 2 bytes + 2 pad + tpage 2 + clutXY 4 + pad 2 + private const int BytesPerTriangle = 52; + + // Per-GameObject entry in splashpack + // offset(4) + position(12) + rotation(36) + polyCount(2) + luaIdx(2) + flags(4) + components(8) + AABB(24) = 92 + private const int BytesPerGameObject = 92; + + // Per-collider entry + private const int BytesPerCollider = 32; + + // Per-interactable entry + private const int BytesPerInteractable = 24; + + // Atlas metadata entry + private const int BytesPerAtlasMetadata = 12; + + // CLUT metadata entry + private const int BytesPerClutMetadata = 12; + + // Audio clip metadata entry (offset + size + sampleRate + flags + nameOffset) + private const int BytesPerAudioMetadata = 16; + + // Renderer ordering table size (from renderer.hh) + private const int OrderingTableSize = 2048 * 3; + + // Renderer bump allocator size (from renderer.hh) + private const int BumpAllocatorSize = 8096 * 24; + + /// + /// Analyze scene data and produce a memory report. + /// Call this after PSXSceneWriter.Write to get accurate stats, + /// or before to get estimates. + /// + public static SceneMemoryReport Analyze( + in PSXSceneWriter.SceneData scene, + string sceneName, + int resolutionWidth, + int resolutionHeight, + bool dualBuffering, + long compiledExeBytes = 0) + { + var report = new SceneMemoryReport(); + report.sceneName = sceneName; + + // ---- Count CLUTs ---- + int clutCount = 0; + foreach (var atlas in scene.atlases) + foreach (var tex in atlas.ContainedTextures) + if (tex.ColorPalette != null) + clutCount++; + + // ---- Count Lua files ---- + var luaFiles = new System.Collections.Generic.List(); + foreach (var exporter in scene.exporters) + if (exporter.LuaFile != null && !luaFiles.Contains(exporter.LuaFile)) + luaFiles.Add(exporter.LuaFile); + if (scene.sceneLuaFile != null && !luaFiles.Contains(scene.sceneLuaFile)) + luaFiles.Add(scene.sceneLuaFile); + + // ---- Count colliders ---- + int colliderCount = 0; + foreach (var e in scene.exporters) + if (e.CollisionType != PSXCollisionType.None) + colliderCount++; + + // ---- Count triangles ---- + int totalTriangles = 0; + foreach (var e in scene.exporters) + totalTriangles += e.Mesh.Triangles.Count; + + // ---- Store counts ---- + report.objectCount = scene.exporters.Length; + report.triangleCount = totalTriangles; + report.atlasCount = scene.atlases.Length; + report.clutCount = clutCount; + report.audioClipCount = scene.audioClips?.Length ?? 0; + report.navRegionCount = scene.navRegionBuilder?.RegionCount ?? 0; + report.roomCount = scene.roomBuilder?.RoomCount ?? 0; + report.portalCount = scene.roomBuilder?.PortalCount ?? 0; + + // ---- Header (v11, ~96 bytes with all extensions) ---- + report.headerBytes = 96; // approximate header size including all version extensions + + // ---- Lua section ---- + long luaBytes = 0; + foreach (var lua in luaFiles) + { + luaBytes += 8; // offset placeholder + size + luaBytes += Align4(Encoding.UTF8.GetByteCount(lua.LuaScript)); + } + report.luaBytes = luaBytes; + + // ---- GameObject section ---- + report.gameObjectBytes = (long)scene.exporters.Length * BytesPerGameObject; + + // ---- Collider section ---- + report.colliderBytes = (long)colliderCount * BytesPerCollider; + + // ---- BVH section ---- + report.bvhBytes = EstimateBvhSize(scene.bvh); + + // ---- Interactable section ---- + report.interactableBytes = (long)scene.interactables.Length * BytesPerInteractable; + + // ---- Collision soup ---- + report.collisionBytes = EstimateCollisionSize(scene.collisionExporter); + + // ---- Nav region data ---- + report.navRegionBytes = EstimateNavRegionSize(scene.navRegionBuilder); + + // ---- Room/portal data ---- + report.roomPortalBytes = EstimateRoomPortalSize(scene.roomBuilder); + + // ---- Atlas metadata ---- + report.atlasMetadataBytes = (long)scene.atlases.Length * BytesPerAtlasMetadata; + + // ---- CLUT metadata ---- + report.clutMetadataBytes = (long)clutCount * BytesPerClutMetadata; + + // ---- Mesh data ---- + report.meshDataBytes = (long)totalTriangles * BytesPerTriangle; + + // ---- Atlas pixel data ---- + long atlasPixelBytes = 0; + foreach (var atlas in scene.atlases) + atlasPixelBytes += Align4((long)atlas.Width * TextureAtlas.Height * 2); + report.atlasPixelBytes = atlasPixelBytes; + + // ---- CLUT data ---- + long clutDataBytes = 0; + foreach (var atlas in scene.atlases) + foreach (var tex in atlas.ContainedTextures) + if (tex.ColorPalette != null) + clutDataBytes += Align4((long)tex.ColorPalette.Count * 2); + report.clutDataBytes = clutDataBytes; + + // ---- Name table ---- + long nameTableBytes = 0; + foreach (var e in scene.exporters) + { + string name = e.gameObject.name; + if (name.Length > 24) name = name.Substring(0, 24); + nameTableBytes += 1 + Encoding.UTF8.GetByteCount(name) + 1; // length byte + name + null + } + report.nameTableBytes = nameTableBytes; + + // ---- Audio metadata + data ---- + int audioClipCount = scene.audioClips?.Length ?? 0; + report.audioMetadataBytes = (long)audioClipCount * BytesPerAudioMetadata; + + long audioDataBytes = 0; + long spuAudioBytes = 0; + if (scene.audioClips != null) + { + foreach (var clip in scene.audioClips) + { + if (clip.adpcmData != null) + { + audioDataBytes += Align4(clip.adpcmData.Length); + spuAudioBytes += clip.adpcmData.Length; + } + // clip name data + string name = clip.clipName ?? ""; + audioDataBytes += name.Length + 1; // name + null + } + } + report.audioDataBytes = audioDataBytes; + report.spuAudioBytes = spuAudioBytes; + + // ---- VRAM breakdown ---- + int fbCount = dualBuffering ? 2 : 1; + report.framebufferVramBytes = (long)resolutionWidth * resolutionHeight * 2 * fbCount; + report.textureVramBytes = atlasPixelBytes; // same data, uploaded to VRAM + report.clutVramBytes = clutDataBytes; + + // ---- Renderer overhead (double-buffered OTs + bump allocators) ---- + long otBytes = 2L * OrderingTableSize * 4; // two OTs, each entry is a pointer (4 bytes) + long bumpBytes = 2L * BumpAllocatorSize; + report.rendererOverhead = otBytes + bumpBytes; + + // ---- Executable size ---- + report.executableBytes = compiledExeBytes > 0 ? compiledExeBytes : 150 * 1024; // estimate if unknown + + return report; + } + + /// + /// Overload that reads the compiled .ps-exe file size if available. + /// + public static SceneMemoryReport Analyze( + in PSXSceneWriter.SceneData scene, + string sceneName, + int resolutionWidth, + int resolutionHeight, + bool dualBuffering, + string compiledExePath) + { + long exeBytes = 0; + if (!string.IsNullOrEmpty(compiledExePath) && System.IO.File.Exists(compiledExePath)) + exeBytes = new System.IO.FileInfo(compiledExePath).Length; + return Analyze(in scene, sceneName, resolutionWidth, resolutionHeight, dualBuffering, exeBytes); + } + + private static long Align4(long size) + { + return (size + 3) & ~3L; + } + + private static long EstimateBvhSize(BVH bvh) + { + if (bvh == null) return 0; + // BVH nodes: each node has AABB (24 bytes) + child/tri info (8 bytes) = 32 bytes + // Triangle refs: 2 bytes each (uint16) + return (long)bvh.NodeCount * 32 + (long)bvh.TriangleRefCount * 2; + } + + private static long EstimateCollisionSize(PSXCollisionExporter collision) + { + if (collision == null || collision.MeshCount == 0) return 0; + // Each collision mesh header: AABB + triangle count (28 bytes) + // Each collision triangle: 3 vertices * 6 bytes = 18 bytes + normal 6 bytes = 24 bytes + return (long)collision.MeshCount * 28 + (long)collision.TriangleCount * 24; + } + + private static long EstimateNavRegionSize(PSXNavRegionBuilder nav) + { + if (nav == null || nav.RegionCount == 0) return 0; + // Region: 84 bytes each (header + vertex data) + // Portal: 20 bytes each + return (long)nav.RegionCount * 84 + (long)nav.PortalCount * 20; + } + + private static long EstimateRoomPortalSize(PSXRoomBuilder rooms) + { + if (rooms == null || rooms.RoomCount == 0) return 0; + // Room data: AABB (24 bytes) + tri ref range (4 bytes) + portal range (4 bytes) = 32 bytes per room + // Portal data: 40 bytes each (with right/up vectors) + // Tri refs: 4 bytes each (uint32) + int roomCount = rooms.RoomCount + 1; // +1 for catch-all room + return (long)roomCount * 32 + (long)rooms.PortalCount * 40 + (long)rooms.TotalTriRefCount * 4; + } + } +} diff --git a/Runtime/SceneMemoryAnalyzer.cs.meta b/Runtime/SceneMemoryAnalyzer.cs.meta new file mode 100644 index 0000000..5f3e6b1 --- /dev/null +++ b/Runtime/SceneMemoryAnalyzer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: edf19eb00f51d7a4dad3f01c6b019432 \ No newline at end of file diff --git a/Runtime/SceneMemoryReport.cs b/Runtime/SceneMemoryReport.cs new file mode 100644 index 0000000..474ef8d --- /dev/null +++ b/Runtime/SceneMemoryReport.cs @@ -0,0 +1,161 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace SplashEdit.RuntimeCode +{ + /// + /// Memory usage breakdown for a single exported scene. + /// Covers Main RAM, VRAM, SPU RAM, and CD storage. + /// + public class SceneMemoryReport + { + // ---- Capacities (PS1 hardware limits) ---- + public const long MainRamCapacity = 2 * 1024 * 1024; // 2 MB + public const long VramCapacity = 1024 * 512 * 2; // 1 MB (1024x512 @ 16bpp) + public const long SpuRamCapacity = 512 * 1024; // 512 KB + public const long CdRomCapacity = 650L * 1024 * 1024; // 650 MB (standard CD) + + // ---- Scene name ---- + public string sceneName; + + // ---- Splashpack section sizes (bytes) ---- + public long headerBytes; + public long luaBytes; + public long gameObjectBytes; + public long colliderBytes; + public long bvhBytes; + public long interactableBytes; + public long collisionBytes; + public long navRegionBytes; + public long roomPortalBytes; + public long atlasMetadataBytes; + public long clutMetadataBytes; + public long meshDataBytes; + public long atlasPixelBytes; + public long clutDataBytes; + public long nameTableBytes; + public long audioMetadataBytes; + public long audioDataBytes; + + // ---- Counts ---- + public int objectCount; + public int triangleCount; + public int atlasCount; + public int clutCount; + public int audioClipCount; + public int navRegionCount; + public int roomCount; + public int portalCount; + + // ---- Main RAM estimate ---- + // Renderer double-buffered overhead (ordering tables + bump allocators) + public long rendererOverhead; + // PSYQo + psxsplash executable code (estimated from .ps-exe if available) + public long executableBytes; + + // ---- VRAM breakdown ---- + public long framebufferVramBytes; + public long textureVramBytes; + public long clutVramBytes; + + // ---- SPU RAM ---- + public const long SpuReservedBytes = 0x1010; // capture buffers + PSYQo dummy sample + public long spuAudioBytes; + + // ---- Computed properties ---- + + public long TotalSplashpackBytes => + headerBytes + luaBytes + gameObjectBytes + colliderBytes + + bvhBytes + interactableBytes + collisionBytes + navRegionBytes + + roomPortalBytes + atlasMetadataBytes + clutMetadataBytes + + meshDataBytes + atlasPixelBytes + clutDataBytes + + nameTableBytes + audioMetadataBytes + audioDataBytes; + + /// + /// Total Main RAM usage estimate. Splashpack data is loaded into heap at runtime. + /// + public long TotalMainRamBytes => + executableBytes + TotalSplashpackBytes + rendererOverhead + 8192 /* stack */ + 16384 /* misc heap */; + + public float MainRamPercent => (float)TotalMainRamBytes / MainRamCapacity; + public long MainRamFree => MainRamCapacity - TotalMainRamBytes; + + public long TotalVramBytes => framebufferVramBytes + textureVramBytes + clutVramBytes; + public float VramPercent => (float)TotalVramBytes / VramCapacity; + public long VramFree => VramCapacity - TotalVramBytes; + + public long TotalSpuBytes => SpuReservedBytes + spuAudioBytes; + public float SpuPercent => (float)TotalSpuBytes / SpuRamCapacity; + public long SpuFree => SpuRamCapacity - TotalSpuBytes; + + // CD storage includes the splashpack file + public long CdSceneBytes => TotalSplashpackBytes; + + // ---- Breakdown lists for UI ---- + + public struct BarSegment + { + public string label; + public long bytes; + public Color color; + } + + public List GetMainRamSegments() + { + var segments = new List(); + if (executableBytes > 0) + segments.Add(new BarSegment { label = "Executable", bytes = executableBytes, color = new Color(0.4f, 0.6f, 0.9f) }); + if (meshDataBytes > 0) + segments.Add(new BarSegment { label = "Mesh Data", bytes = meshDataBytes, color = new Color(0.3f, 0.85f, 0.45f) }); + if (atlasPixelBytes > 0) + segments.Add(new BarSegment { label = "Texture Data", bytes = atlasPixelBytes, color = new Color(0.95f, 0.75f, 0.2f) }); + if (audioDataBytes > 0) + segments.Add(new BarSegment { label = "Audio Data", bytes = audioDataBytes, color = new Color(0.85f, 0.3f, 0.65f) }); + + long otherSplashpack = TotalSplashpackBytes - meshDataBytes - atlasPixelBytes - audioDataBytes; + if (otherSplashpack > 0) + segments.Add(new BarSegment { label = "Scene Metadata", bytes = otherSplashpack, color = new Color(0.6f, 0.6f, 0.65f) }); + + if (rendererOverhead > 0) + segments.Add(new BarSegment { label = "Renderer Buffers", bytes = rendererOverhead, color = new Color(0.3f, 0.85f, 0.95f) }); + + long misc = 8192 + 16384; // stack + misc heap + segments.Add(new BarSegment { label = "Stack + Misc", bytes = misc, color = new Color(0.45f, 0.45f, 0.5f) }); + + return segments; + } + + public List GetVramSegments() + { + var segments = new List(); + if (framebufferVramBytes > 0) + segments.Add(new BarSegment { label = "Framebuffers", bytes = framebufferVramBytes, color = new Color(0.9f, 0.3f, 0.35f) }); + if (textureVramBytes > 0) + segments.Add(new BarSegment { label = "Texture Atlases", bytes = textureVramBytes, color = new Color(0.95f, 0.75f, 0.2f) }); + if (clutVramBytes > 0) + segments.Add(new BarSegment { label = "CLUTs", bytes = clutVramBytes, color = new Color(0.85f, 0.5f, 0.2f) }); + return segments; + } + + public List GetSpuSegments() + { + var segments = new List(); + segments.Add(new BarSegment { label = "Reserved", bytes = SpuReservedBytes, color = new Color(0.45f, 0.45f, 0.5f) }); + if (spuAudioBytes > 0) + segments.Add(new BarSegment { label = "Audio Clips", bytes = spuAudioBytes, color = new Color(0.85f, 0.3f, 0.65f) }); + return segments; + } + + /// + /// Get a severity color for a usage percentage. + /// + public static Color GetUsageColor(float percent) + { + if (percent < 0.6f) return new Color(0.35f, 0.85f, 0.45f); // green + if (percent < 0.8f) return new Color(0.95f, 0.75f, 0.2f); // yellow + if (percent < 0.95f) return new Color(0.95f, 0.5f, 0.2f); // orange + return new Color(0.9f, 0.3f, 0.35f); // red + } + } +} diff --git a/Runtime/SceneMemoryReport.cs.meta b/Runtime/SceneMemoryReport.cs.meta new file mode 100644 index 0000000..ea20688 --- /dev/null +++ b/Runtime/SceneMemoryReport.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 09443b447544693488a25a9dfbf8714b \ No newline at end of file