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

@@ -84,13 +84,16 @@ namespace SplashEdit.RuntimeCode
foreach (var exporter in exporters)
{
// Dynamic objects are handled by the runtime collision system, skip them
if (!exporter.StaticCollider && exporter.CollisionType != PSXCollisionType.None)
continue;
PSXCollisionType effectiveType = exporter.CollisionType;
if (effectiveType == PSXCollisionType.None)
{
if (autoIncludeSolid)
{
// Auto-include as Solid so all geometry blocks the player
effectiveType = PSXCollisionType.Solid;
autoIncluded++;
}
@@ -146,8 +149,7 @@ namespace SplashEdit.RuntimeCode
flags = (byte)PSXSurfaceFlag.Solid;
// Check if stairs (tagged on exporter or steep-ish)
if (exporter.ObjectFlags.HasFlag(PSXObjectFlags.Static) &&
dotUp < 0.95f && dotUp > cosWalkable)
if (dotUp < 0.95f && dotUp > cosWalkable)
{
flags |= (byte)PSXSurfaceFlag.Stairs;
}

View File

@@ -5,31 +5,12 @@ using UnityEngine.Serialization;
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// Collision type for PS1 runtime
/// </summary>
public enum PSXCollisionType
{
None = 0, // No collision
Solid = 1, // Solid collision - blocks movement
Trigger = 2, // Trigger - fires events but doesn't block
Platform = 3 // Platform - solid from above, passable from below
}
/// <summary>
/// Object behavior flags for PS1 runtime
/// </summary>
[System.Flags]
public enum PSXObjectFlags
{
None = 0,
Static = 1 << 0, // Object never moves (can be optimized)
Dynamic = 1 << 1, // Object can move
Visible = 1 << 2, // Object is rendered
CastsShadow = 1 << 3, // Object casts shadows (future)
ReceivesShadow = 1 << 4, // Object receives shadows (future)
Interactable = 1 << 5, // Player can interact with this
AlwaysRender = 1 << 6, // Skip frustum culling for this object
Solid = 1,
Trigger = 2,
Platform = 3
}
[RequireComponent(typeof(Renderer))]
@@ -43,184 +24,70 @@ namespace SplashEdit.RuntimeCode
public List<PSXTexture2D> Textures { get; set; } = new List<PSXTexture2D>();
public PSXMesh Mesh { get; protected set; }
[Header("Export Settings")]
[FormerlySerializedAs("BitDepth")]
[SerializeField] private PSXBPP bitDepth = PSXBPP.TEX_8BIT;
[SerializeField] private LuaFile luaFile;
[Header("Object Flags")]
[SerializeField] private PSXObjectFlags objectFlags = PSXObjectFlags.Static | PSXObjectFlags.Visible;
[Header("Collision Settings")]
[SerializeField] private PSXCollisionType collisionType = PSXCollisionType.None;
[SerializeField] private bool staticCollider = true;
[SerializeField] private bool exportCollisionMesh = false;
[SerializeField] private Mesh customCollisionMesh; // Optional simplified collision mesh
[Tooltip("Layer mask for collision detection (1-8)")]
[SerializeField] private Mesh customCollisionMesh;
[Range(1, 8)]
[SerializeField] private int collisionLayer = 1;
[Header("Navigation")]
[Tooltip("Include this object's walkable surfaces in nav region generation")]
[SerializeField] private bool generateNavigation = false;
[Header("Gizmo Settings")]
[FormerlySerializedAs("PreviewNormals")]
[SerializeField] private bool previewNormals = false;
[SerializeField] private float normalPreviewLength = 0.5f;
[SerializeField] private bool showCollisionBounds = true;
// Public accessors for editor and export
public PSXBPP BitDepth => bitDepth;
public PSXCollisionType CollisionType => collisionType;
public bool StaticCollider => staticCollider;
public bool ExportCollisionMesh => exportCollisionMesh;
public Mesh CustomCollisionMesh => customCollisionMesh;
public int CollisionLayer => collisionLayer;
public PSXObjectFlags ObjectFlags => objectFlags;
public bool GenerateNavigation => generateNavigation;
// For assigning texture from editor
public Texture2D texture;
private readonly Dictionary<(int, PSXBPP), PSXTexture2D> cache = new();
private void OnDrawGizmos()
{
if (previewNormals)
{
MeshFilter filter = GetComponent<MeshFilter>();
if (filter != null)
{
Mesh mesh = filter.sharedMesh;
Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
Gizmos.color = Color.green;
for (int i = 0; i < vertices.Length; i++)
{
Vector3 worldVertex = transform.TransformPoint(vertices[i]);
Vector3 worldNormal = transform.TransformDirection(normals[i]);
Gizmos.DrawLine(worldVertex, worldVertex + worldNormal * normalPreviewLength);
}
}
}
}
private void OnDrawGizmosSelected()
{
// Draw collision bounds when object is selected
if (showCollisionBounds && collisionType != PSXCollisionType.None)
{
MeshFilter filter = GetComponent<MeshFilter>();
Mesh collisionMesh = customCollisionMesh != null ? customCollisionMesh : (filter?.sharedMesh);
if (collisionMesh != null)
{
Bounds bounds = collisionMesh.bounds;
// Choose color based on collision type
switch (collisionType)
{
case PSXCollisionType.Solid:
Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.5f); // Red
break;
case PSXCollisionType.Trigger:
Gizmos.color = new Color(0.3f, 1f, 0.3f, 0.5f); // Green
break;
case PSXCollisionType.Platform:
Gizmos.color = new Color(0.3f, 0.3f, 1f, 0.5f); // Blue
break;
}
// Draw AABB
Matrix4x4 oldMatrix = Gizmos.matrix;
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.DrawWireCube(bounds.center, bounds.size);
// Draw filled with lower alpha
Color fillColor = Gizmos.color;
fillColor.a = 0.1f;
Gizmos.color = fillColor;
Gizmos.DrawCube(bounds.center, bounds.size);
Gizmos.matrix = oldMatrix;
}
}
}
public void CreatePSXTextures2D()
{
Renderer renderer = GetComponent<Renderer>();
Textures.Clear();
if (renderer != null)
if (renderer == null) return;
Material[] materials = renderer.sharedMaterials;
foreach (Material mat in materials)
{
// If an override texture is set, use it for all submeshes
if (texture != null)
if (mat == null || mat.mainTexture == null) continue;
Texture mainTexture = mat.mainTexture;
Texture2D tex2D = mainTexture is Texture2D existing
? existing
: ConvertToTexture2D(mainTexture);
if (tex2D == null) continue;
if (cache.TryGetValue((tex2D.GetInstanceID(), bitDepth), out var cached))
{
PSXTexture2D tex;
if (cache.ContainsKey((texture.GetInstanceID(), bitDepth)))
{
tex = cache[(texture.GetInstanceID(), bitDepth)];
}
else
{
tex = PSXTexture2D.CreateFromTexture2D(texture, bitDepth);
tex.OriginalTexture = texture;
cache.Add((texture.GetInstanceID(), bitDepth), tex);
}
Textures.Add(tex);
return;
Textures.Add(cached);
}
Material[] materials = renderer.sharedMaterials;
foreach (Material mat in materials)
else
{
if (mat != null && mat.mainTexture != null)
{
Texture mainTexture = mat.mainTexture;
Texture2D tex2D = null;
if (mainTexture is Texture2D existingTex2D)
{
tex2D = existingTex2D;
}
else
{
tex2D = ConvertToTexture2D(mainTexture);
}
if (tex2D != null)
{
PSXTexture2D tex;
if (cache.ContainsKey((tex2D.GetInstanceID(), bitDepth)))
{
tex = cache[(tex2D.GetInstanceID(), bitDepth)];
}
else
{
tex = PSXTexture2D.CreateFromTexture2D(tex2D, bitDepth);
tex.OriginalTexture = tex2D;
cache.Add((tex2D.GetInstanceID(), bitDepth), tex);
}
Textures.Add(tex);
}
}
var tex = PSXTexture2D.CreateFromTexture2D(tex2D, bitDepth);
tex.OriginalTexture = tex2D;
cache.Add((tex2D.GetInstanceID(), bitDepth), tex);
Textures.Add(tex);
}
}
}
private Texture2D ConvertToTexture2D(Texture texture)
private static Texture2D ConvertToTexture2D(Texture src)
{
Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
Texture2D texture2D = new Texture2D(src.width, src.height, TextureFormat.RGBA32, false);
RenderTexture currentActiveRT = RenderTexture.active;
RenderTexture.active = texture as RenderTexture;
RenderTexture.active = src as RenderTexture;
texture2D.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0);
texture2D.ReadPixels(new Rect(0, 0, src.width, src.height), 0, 0);
texture2D.Apply();
RenderTexture.active = currentActiveRT;

View File

@@ -9,6 +9,11 @@ using UnityEngine;
namespace SplashEdit.RuntimeCode
{
public enum PSXSceneType
{
Exterior = 0,
Interior = 1
}
[ExecuteInEditMode]
public class PSXSceneExporter : MonoBehaviour
@@ -35,7 +40,7 @@ namespace SplashEdit.RuntimeCode
[Header("Scene Type")]
[Tooltip("Exterior uses BVH frustum culling. Interior uses room/portal occlusion.")]
public int SceneType = 0; // 0=exterior, 1=interior
public PSXSceneType SceneType = PSXSceneType.Exterior;
[Header("Cutscenes")]
[Tooltip("Cutscene clips to include in this scene's splashpack. Only these will be exported.")]
@@ -166,7 +171,7 @@ namespace SplashEdit.RuntimeCode
// Collect them early so both systems use the same room indices.
PSXRoom[] rooms = null;
PSXPortalLink[] portalLinks = null;
if (SceneType == 1)
if (SceneType == PSXSceneType.Interior)
{
rooms = FindObjectsByType<PSXRoom>(FindObjectsSortMode.None);
portalLinks = FindObjectsByType<PSXPortalLink>(FindObjectsSortMode.None);
@@ -194,7 +199,7 @@ namespace SplashEdit.RuntimeCode
// Phase 5: Build room/portal system (for interior scenes)
_roomBuilder = new PSXRoomBuilder();
if (SceneType == 1)
if (SceneType == PSXSceneType.Interior)
{
if (rooms != null && rooms.Length > 0)
{

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);