Broken RUntime

This commit is contained in:
Jan Racek
2026-03-27 13:47:18 +01:00
parent 6bf74fa929
commit d29ef569b3
16 changed files with 1168 additions and 1371 deletions

View File

@@ -50,7 +50,7 @@ namespace SplashEdit.RuntimeCode
public float gravity;
// Scene configuration (v11)
public int sceneType; // 0=exterior, 1=interior
public PSXSceneType sceneType;
public bool fogEnabled;
public Color fogColor;
public int fogDensity; // 1-10
@@ -111,7 +111,12 @@ namespace SplashEdit.RuntimeCode
int colliderCount = 0;
foreach (var e in scene.exporters)
{
if (e.CollisionType != PSXCollisionType.None)
if (e.CollisionType == PSXCollisionType.None || e.StaticCollider)
continue;
Mesh cm = e.CustomCollisionMesh != null
? e.CustomCollisionMesh
: e.GetComponent<MeshFilter>()?.sharedMesh;
if (cm != null)
colliderCount++;
}
@@ -121,17 +126,17 @@ namespace SplashEdit.RuntimeCode
exporterIndex[scene.exporters[i]] = i;
// ──────────────────────────────────────────────────────
// Header (72 bytes total — splashpack v8)
// Header (104 bytes — splashpack v15)
// ──────────────────────────────────────────────────────
writer.Write('S');
writer.Write('P');
writer.Write((ushort)13); // version
writer.Write((ushort)15);
writer.Write((ushort)luaFiles.Count);
writer.Write((ushort)scene.exporters.Length);
writer.Write((ushort)0); // navmeshCount (legacy)
writer.Write((ushort)scene.atlases.Length);
writer.Write((ushort)clutCount);
writer.Write((ushort)colliderCount);
writer.Write((ushort)scene.interactables.Length);
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerPos.x, gte));
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(-scene.playerPos.y, gte));
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerPos.z, gte));
@@ -142,37 +147,23 @@ namespace SplashEdit.RuntimeCode
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerHeight, gte));
// Scene Lua index
if (scene.sceneLuaFile != null)
writer.Write((short)luaFiles.IndexOf(scene.sceneLuaFile));
else
writer.Write((short)-1);
// BVH info
writer.Write((ushort)scene.bvh.NodeCount);
writer.Write((ushort)scene.bvh.TriangleRefCount);
// Component counts (version 4)
writer.Write((ushort)scene.interactables.Length);
writer.Write((ushort)0); // healthCount (removed)
writer.Write((ushort)0); // timerCount (removed)
writer.Write((ushort)0); // spawnerCount (removed)
writer.Write((ushort)scene.sceneType);
writer.Write((ushort)0); // pad0
// NavGrid (version 5, legacy)
writer.Write((ushort)0);
writer.Write((ushort)0);
// Scene type (version 6)
writer.Write((ushort)scene.sceneType); // 0=exterior, 1=interior
writer.Write((ushort)0);
// World collision + nav regions (version 7)
writer.Write((ushort)scene.collisionExporter.MeshCount);
writer.Write((ushort)scene.collisionExporter.TriangleCount);
writer.Write((ushort)scene.navRegionBuilder.RegionCount);
writer.Write((ushort)scene.navRegionBuilder.PortalCount);
// Movement parameters (version 8, 12 bytes)
// Movement parameters (12 bytes)
{
const float fps = 30f;
float movePerFrame = scene.moveSpeed / fps / gte;
@@ -187,52 +178,49 @@ namespace SplashEdit.RuntimeCode
writer.Write((ushort)Mathf.Clamp(Mathf.RoundToInt(gravPsx * 4096f), 0, 65535));
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerRadius, gte));
writer.Write((ushort)0); // padding
writer.Write((ushort)0); // pad1
}
// Name table offset placeholder (version 9, 4 bytes)
long nameTableOffsetPos = writer.BaseStream.Position;
writer.Write((uint)0); // placeholder for name table offset
writer.Write((uint)0);
// Audio clip info (version 10, 8 bytes)
int audioClipCount = scene.audioClips?.Length ?? 0;
writer.Write((ushort)audioClipCount);
writer.Write((ushort)0); // padding
writer.Write((ushort)0); // pad2
long audioTableOffsetPos = writer.BaseStream.Position;
writer.Write((uint)0); // placeholder for audio table offset
writer.Write((uint)0);
// Fog + room/portal header (version 11, 12 bytes)
{
writer.Write((byte)(scene.fogEnabled ? 1 : 0));
writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.r * 255f), 0, 255));
writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.g * 255f), 0, 255));
writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.b * 255f), 0, 255));
writer.Write((byte)Mathf.Clamp(scene.fogDensity, 1, 10));
writer.Write((byte)0); // reserved
writer.Write((byte)0); // pad3
int roomCount = scene.roomBuilder?.RoomCount ?? 0;
int portalCount = scene.roomBuilder?.PortalCount ?? 0;
int roomTriRefCount = scene.roomBuilder?.TotalTriRefCount ?? 0;
// roomCount is the room count NOT including catch-all; the binary adds +1 for it
writer.Write((ushort)(roomCount > 0 ? roomCount + 1 : 0));
writer.Write((ushort)portalCount);
writer.Write((ushort)roomTriRefCount);
}
// Cutscene header (version 12, 8 bytes)
int cutsceneCount = scene.cutscenes?.Length ?? 0;
writer.Write((ushort)cutsceneCount);
writer.Write((ushort)0); // reserved_cs
writer.Write((ushort)0); // pad4
long cutsceneTableOffsetPos = writer.BaseStream.Position;
writer.Write((uint)0); // cutsceneTableOffset placeholder
// UI canvas header (version 13, 8 bytes)
int uiCanvasCount = scene.canvases?.Length ?? 0;
int uiFontCount = scene.fonts?.Length ?? 0;
writer.Write((ushort)uiCanvasCount);
writer.Write((byte)uiFontCount); // was uiReserved low byte
writer.Write((byte)0); // was uiReserved high byte
writer.Write((byte)uiFontCount);
writer.Write((byte)0); // uiPad5
long uiTableOffsetPos = writer.BaseStream.Position;
writer.Write((uint)0); // uiTableOffset placeholder
writer.Write((uint)0);
long pixelDataOffsetPos = writer.BaseStream.Position;
writer.Write((uint)0); // pixelDataOffset placeholder
// ──────────────────────────────────────────────────────
// Lua file metadata
@@ -295,7 +283,7 @@ namespace SplashEdit.RuntimeCode
for (int exporterIdx = 0; exporterIdx < scene.exporters.Length; exporterIdx++)
{
PSXObjectExporter exporter = scene.exporters[exporterIdx];
if (exporter.CollisionType == PSXCollisionType.None)
if (exporter.CollisionType == PSXCollisionType.None || exporter.StaticCollider)
continue;
MeshFilter meshFilter = exporter.GetComponent<MeshFilter>();
@@ -483,33 +471,6 @@ namespace SplashEdit.RuntimeCode
}
}
// Atlas pixel data
foreach (TextureAtlas atlas in scene.atlases)
{
AlignToFourBytes(writer);
atlasOffset.DataOffsets.Add(writer.BaseStream.Position);
for (int y = 0; y < atlas.vramPixels.GetLength(1); y++)
for (int x = 0; x < atlas.vramPixels.GetLength(0); x++)
writer.Write(atlas.vramPixels[x, y].Pack());
}
// CLUT data
foreach (TextureAtlas atlas in scene.atlases)
{
foreach (var texture in atlas.ContainedTextures)
{
if (texture.ColorPalette != null)
{
AlignToFourBytes(writer);
clutOffset.DataOffsets.Add(writer.BaseStream.Position);
foreach (VRAMPixel color in texture.ColorPalette)
writer.Write((ushort)color.Pack());
}
}
}
// ──────────────────────────────────────────────────────
// Object name table (version 9)
// ──────────────────────────────────────────────────────
@@ -535,46 +496,50 @@ namespace SplashEdit.RuntimeCode
// ──────────────────────────────────────────────────────
// Audio clip data (version 10)
// Metadata entries are 16 bytes each, written contiguously.
// Name strings follow the metadata block with backfilled offsets.
// ADPCM blobs deferred to dead zone.
// ──────────────────────────────────────────────────────
List<long> audioDataOffsetPositions = new List<long>();
if (audioClipCount > 0 && scene.audioClips != null)
{
// Write audio table: per clip metadata (12 bytes each)
AlignToFourBytes(writer);
long audioTableStart = writer.BaseStream.Position;
// First pass: write metadata placeholders (16 bytes each)
List<long> audioDataOffsetPositions = new List<long>();
List<long> audioNameOffsetPositions = new List<long>();
List<string> audioClipNames = new List<string>();
// Phase 1: Write all 16-byte metadata entries contiguously
for (int i = 0; i < audioClipCount; i++)
{
var clip = scene.audioClips[i];
audioDataOffsetPositions.Add(writer.BaseStream.Position);
writer.Write((uint)0); // dataOffset placeholder
writer.Write((uint)(clip.adpcmData?.Length ?? 0)); // sizeBytes
writer.Write((ushort)clip.sampleRate);
string name = clip.clipName ?? "";
if (name.Length > 255) name = name.Substring(0, 255);
audioDataOffsetPositions.Add(writer.BaseStream.Position);
writer.Write((uint)0); // dataOffset placeholder (backfilled in dead zone)
writer.Write((uint)(clip.adpcmData?.Length ?? 0));
writer.Write((ushort)clip.sampleRate);
writer.Write((byte)(clip.loop ? 1 : 0));
writer.Write((byte)System.Math.Min(name.Length, 255));
writer.Write((byte)name.Length);
audioNameOffsetPositions.Add(writer.BaseStream.Position);
writer.Write((uint)0); // nameOffset placeholder
audioClipNames.Add(name);
}
// Second pass: write ADPCM data and backfill offsets
// Phase 2: Write name strings (after all metadata entries)
for (int i = 0; i < audioClipCount; i++)
{
byte[] data = scene.audioClips[i].adpcmData;
if (data != null && data.Length > 0)
{
AlignToFourBytes(writer);
long dataPos = writer.BaseStream.Position;
writer.Write(data);
string name = audioClipNames[i];
long namePos = writer.BaseStream.Position;
byte[] nameBytes = System.Text.Encoding.ASCII.GetBytes(name);
writer.Write(nameBytes);
writer.Write((byte)0);
// Backfill data offset
long curPos = writer.BaseStream.Position;
writer.Seek((int)audioDataOffsetPositions[i], SeekOrigin.Begin);
writer.Write((uint)dataPos);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
long curPos = writer.BaseStream.Position;
writer.Seek((int)audioNameOffsetPositions[i], SeekOrigin.Begin);
writer.Write((uint)namePos);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
// Backfill audio table offset in header
@@ -584,28 +549,6 @@ namespace SplashEdit.RuntimeCode
writer.Write((uint)audioTableStart);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
int totalAudioBytes = 0;
foreach (var clip in scene.audioClips)
if (clip.adpcmData != null) totalAudioBytes += clip.adpcmData.Length;
// Third pass: write audio clip names and backfill name offsets
for (int i = 0; i < audioClipCount; i++)
{
string name = scene.audioClips[i].clipName ?? "";
if (name.Length > 255) name = name.Substring(0, 255);
long namePos = writer.BaseStream.Position;
byte[] nameBytes = System.Text.Encoding.ASCII.GetBytes(name);
writer.Write(nameBytes);
writer.Write((byte)0); // null terminator
long curPos = writer.BaseStream.Position;
writer.Seek((int)audioNameOffsetPositions[i], SeekOrigin.Begin);
writer.Write((uint)namePos);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
log?.Invoke($"{audioClipCount} audio clips ({totalAudioBytes / 1024}KB ADPCM) written.", LogType.Log);
}
// ──────────────────────────────────────────────────────
@@ -635,11 +578,12 @@ namespace SplashEdit.RuntimeCode
// ──────────────────────────────────────────────────────
// UI canvas + font data (version 13)
// Font descriptors: 112 bytes each (before canvas data)
// Font pixel data: raw 4bpp (after font descriptors)
// Canvas descriptor table: 12 bytes per canvas
// Element records: 48 bytes each
// Name and text strings follow with offset backfill
// Font pixel data is deferred to the dead zone.
// ──────────────────────────────────────────────────────
List<long> fontDataOffsetPositions = new List<long>();
if ((uiCanvasCount > 0 && scene.canvases != null) || uiFontCount > 0)
{
AlignToFourBytes(writer);
@@ -648,7 +592,6 @@ namespace SplashEdit.RuntimeCode
// ── Font descriptors (112 bytes each) ──
// Layout: glyphW(1) glyphH(1) vramX(2) vramY(2) textureH(2)
// dataOffset(4) dataSize(4)
List<long> fontDataOffsetPositions = new List<long>();
if (scene.fonts != null)
{
foreach (var font in scene.fonts)
@@ -669,32 +612,9 @@ namespace SplashEdit.RuntimeCode
}
}
// ── Font pixel data (raw 4bpp) ──
if (scene.fonts != null)
{
for (int fi = 0; fi < scene.fonts.Length; fi++)
{
var font = scene.fonts[fi];
if (font.PixelData == null || font.PixelData.Length == 0) continue;
AlignToFourBytes(writer);
long dataPos = writer.BaseStream.Position;
writer.Write(font.PixelData);
// Backfill data offset
long curPos = writer.BaseStream.Position;
writer.Seek((int)fontDataOffsetPositions[fi], SeekOrigin.Begin);
writer.Write((uint)dataPos);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
if (scene.fonts.Length > 0)
{
int totalFontBytes = 0;
foreach (var f in scene.fonts) totalFontBytes += f.PixelData?.Length ?? 0;
log?.Invoke($"{scene.fonts.Length} custom font(s) written ({totalFontBytes} bytes 4bpp data).", LogType.Log);
}
}
// Font pixel data is deferred to the dead zone (after pixelDataOffset).
// The C++ loader reads font pixel data via the dataOffset, uploads to VRAM,
// then never accesses it again.
// ── Canvas descriptor table (12 bytes each) ──
// Layout per descriptor:
@@ -873,6 +793,96 @@ namespace SplashEdit.RuntimeCode
log?.Invoke($"{uiCanvasCount} UI canvases ({totalElements} elements) written.", LogType.Log);
}
// ══════════════════════════════════════════════════════
// DEAD ZONE — pixel/audio bulk data (freed after VRAM/SPU upload)
// Everything written after this point is not needed at runtime.
// ══════════════════════════════════════════════════════
AlignToFourBytes(writer);
long pixelDataStart = writer.BaseStream.Position;
// Atlas pixel data
foreach (TextureAtlas atlas in scene.atlases)
{
AlignToFourBytes(writer);
atlasOffset.DataOffsets.Add(writer.BaseStream.Position);
for (int y = 0; y < atlas.vramPixels.GetLength(1); y++)
for (int x = 0; x < atlas.vramPixels.GetLength(0); x++)
writer.Write(atlas.vramPixels[x, y].Pack());
}
// CLUT data
foreach (TextureAtlas atlas in scene.atlases)
{
foreach (var texture in atlas.ContainedTextures)
{
if (texture.ColorPalette != null)
{
AlignToFourBytes(writer);
clutOffset.DataOffsets.Add(writer.BaseStream.Position);
foreach (VRAMPixel color in texture.ColorPalette)
writer.Write((ushort)color.Pack());
}
}
}
// Audio ADPCM data
if (audioClipCount > 0 && scene.audioClips != null)
{
for (int i = 0; i < audioClipCount; i++)
{
byte[] data = scene.audioClips[i].adpcmData;
if (data != null && data.Length > 0)
{
AlignToFourBytes(writer);
long dataPos = writer.BaseStream.Position;
writer.Write(data);
long curPos = writer.BaseStream.Position;
writer.Seek((int)audioDataOffsetPositions[i], SeekOrigin.Begin);
writer.Write((uint)dataPos);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
}
int totalAudioBytes = 0;
foreach (var clip in scene.audioClips)
if (clip.adpcmData != null) totalAudioBytes += clip.adpcmData.Length;
log?.Invoke($"{audioClipCount} audio clips ({totalAudioBytes / 1024}KB ADPCM) written.", LogType.Log);
}
// Font pixel data
if (scene.fonts != null)
{
for (int fi = 0; fi < scene.fonts.Length; fi++)
{
var font = scene.fonts[fi];
if (font.PixelData == null || font.PixelData.Length == 0) continue;
AlignToFourBytes(writer);
long dataPos = writer.BaseStream.Position;
writer.Write(font.PixelData);
long curPos = writer.BaseStream.Position;
writer.Seek((int)fontDataOffsetPositions[fi], SeekOrigin.Begin);
writer.Write((uint)dataPos);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
}
// Backfill pixelDataOffset in header
{
long curPos = writer.BaseStream.Position;
writer.Seek((int)pixelDataOffsetPos, SeekOrigin.Begin);
writer.Write((uint)pixelDataStart);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
long totalSize = writer.BaseStream.Position;
long deadBytes = totalSize - pixelDataStart;
log?.Invoke($"Pixel/audio dead zone: {deadBytes / 1024}KB (freed after VRAM/SPU upload).", LogType.Log);
// Backfill offsets
BackfillOffsets(writer, luaOffset, "lua", log);
BackfillOffsets(writer, meshOffset, "mesh", log);