memory reports

This commit is contained in:
Jan Racek
2026-03-27 19:29:41 +01:00
parent 24d0c1fa07
commit 45a552be5a
9 changed files with 823 additions and 2 deletions

View File

@@ -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 ──
/// <summary>Texture atlases from the last export (null before first export).</summary>
public TextureAtlas[] LastExportAtlases => _atlases;
/// <summary>Custom font data from the last export.</summary>
public PSXFontData[] LastExportFonts => _fonts;
/// <summary>Audio clip ADPCM sizes from the last export.</summary>
public long[] LastExportAudioSizes => _lastAudioSizes;
private long[] _lastAudioSizes;
/// <summary>Total triangle count from the last export.</summary>
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,

View File

@@ -0,0 +1,255 @@
using System.Text;
using UnityEngine;
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// Computes a SceneMemoryReport from scene export data.
/// All sizes match PSXSceneWriter's binary layout exactly.
/// </summary>
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;
/// <summary>
/// Analyze scene data and produce a memory report.
/// Call this after PSXSceneWriter.Write to get accurate stats,
/// or before to get estimates.
/// </summary>
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<LuaFile>();
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;
}
/// <summary>
/// Overload that reads the compiled .ps-exe file size if available.
/// </summary>
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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: edf19eb00f51d7a4dad3f01c6b019432

View File

@@ -0,0 +1,161 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// Memory usage breakdown for a single exported scene.
/// Covers Main RAM, VRAM, SPU RAM, and CD storage.
/// </summary>
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;
/// <summary>
/// Total Main RAM usage estimate. Splashpack data is loaded into heap at runtime.
/// </summary>
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<BarSegment> GetMainRamSegments()
{
var segments = new List<BarSegment>();
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<BarSegment> GetVramSegments()
{
var segments = new List<BarSegment>();
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<BarSegment> GetSpuSegments()
{
var segments = new List<BarSegment>();
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;
}
/// <summary>
/// Get a severity color for a usage percentage.
/// </summary>
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
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 09443b447544693488a25a9dfbf8714b