memory reports
This commit is contained in:
183
Editor/Core/SceneMemoryReport.cs
Normal file
183
Editor/Core/SceneMemoryReport.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SplashEdit.EditorCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Memory analysis report for a single exported scene.
|
||||
/// All values are in bytes unless noted otherwise.
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <summary>RAM used by scene data (live portion of splashpack).</summary>
|
||||
public long SceneRamUsage => splashpackLiveSize > 0 ? splashpackLiveSize : splashpackFileSize;
|
||||
|
||||
/// <summary>Total estimated RAM: fixed overhead + scene data. Does NOT include code/BSS.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a SceneMemoryReport by reading the exported splashpack binary header
|
||||
/// and the scene's VRAM/audio data.
|
||||
/// </summary>
|
||||
public static class SceneMemoryAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// Analyze an exported scene. Call after ExportToPath().
|
||||
/// </summary>
|
||||
/// <param name="sceneName">Display name for the scene.</param>
|
||||
/// <param name="splashpackPath">Path to the exported .splashpack file.</param>
|
||||
/// <param name="loaderPackPath">Path to the loading screen file (may be null).</param>
|
||||
/// <param name="atlases">Texture atlases from the export pipeline.</param>
|
||||
/// <param name="audioExportSizes">Array of ADPCM byte sizes per audio clip.</param>
|
||||
/// <param name="fonts">Custom font descriptors.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Core/SceneMemoryReport.cs.meta
Normal file
2
Editor/Core/SceneMemoryReport.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f68ba273eb88c3b4796e43f40b226c71
|
||||
@@ -42,6 +42,10 @@ namespace SplashEdit.EditorCode
|
||||
// ───── Scene List ─────
|
||||
private List<SceneEntry> _sceneList = new List<SceneEntry>();
|
||||
|
||||
// ───── Memory Reports ─────
|
||||
private List<SceneMemoryReport> _memoryReports = new List<SceneMemoryReport>();
|
||||
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(
|
||||
$"<b>{report.gameObjectCount}</b> objects | " +
|
||||
$"<b>{report.triangleCount}</b> tris | " +
|
||||
$"<b>{report.atlasCount}</b> atlases | " +
|
||||
$"<b>{report.clutCount}</b> 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<string, string>();
|
||||
_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();
|
||||
|
||||
@@ -124,6 +124,18 @@ namespace SplashEdit.EditorCode
|
||||
set => EditorPrefs.SetBool(Prefix + "CleanBuild", value);
|
||||
}
|
||||
|
||||
// --- Memory Overlay ---
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static bool MemoryOverlay
|
||||
{
|
||||
get => EditorPrefs.GetBool(Prefix + "MemoryOverlay", false);
|
||||
set => EditorPrefs.SetBool(Prefix + "MemoryOverlay", value);
|
||||
}
|
||||
|
||||
// --- Export settings ---
|
||||
public static float DefaultGTEScaling
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user