using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using SplashEdit.RuntimeCode;
using Debug = UnityEngine.Debug;
namespace SplashEdit.EditorCode
{
///
/// SplashEdit Control Panel — the single unified window for the entire pipeline.
/// One window. One button. Everything works.
///
public class SplashControlPanel : EditorWindow
{
// ───── Constants ─────
private const string WINDOW_TITLE = "SplashEdit Control Panel";
private const string MENU_PATH = "PlayStation 1/SplashEdit Control Panel %#l";
// ───── UI State ─────
private Vector2 _scrollPos;
private int _selectedTab = 0;
private static readonly string[] _tabNames = { "Dependencies", "Scenes", "Build" };
private bool _showNativeProject = true;
private bool _showToolchainSection = true;
private bool _showScenesSection = true;
private bool _showVRAMSection = true;
private bool _showBuildSection = true;
// ───── Build State ─────
private static bool _isBuilding;
private static bool _isRunning;
private static Process _emulatorProcess;
// ───── 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;
private bool _hasRedux;
private bool _hasNativeProject;
private bool _hasPsxavenc;
private bool _hasMkpsxiso;
private string _reduxVersion = "";
// ───── Native project installer ─────
private bool _isInstallingNative;
private string _nativeInstallStatus = "";
private string _manualNativePath = "";
// PCdrv serial host instance (for real hardware file serving)
private static PCdrvSerialHost _pcdrvHost;
private struct SceneEntry
{
public SceneAsset asset;
public string path;
public string name;
}
// ═══════════════════════════════════════════════════════════════
// Menu & Window Lifecycle
// ═══════════════════════════════════════════════════════════════
[MenuItem(MENU_PATH, false, 0)]
public static void ShowWindow()
{
var window = GetWindow();
window.titleContent = new GUIContent(WINDOW_TITLE, EditorGUIUtility.IconContent("d_BuildSettings.PSP2.Small").image);
window.minSize = new Vector2(420, 600);
window.Show();
}
private void OnEnable()
{
SplashBuildPaths.EnsureDirectories();
RefreshToolchainStatus();
LoadSceneList();
_manualNativePath = SplashSettings.NativeProjectPath;
}
private void OnDisable()
{
}
private void OnFocus()
{
RefreshToolchainStatus();
}
// ═══════════════════════════════════════════════════════════════
// Main GUI
// ═══════════════════════════════════════════════════════════════
private void OnGUI()
{
if (_isRunning && _pcdrvHost != null && !_pcdrvHost.IsRunning)
{
StopAll();
Log("PCdrv host connection lost.", LogType.Warning);
}
DrawHeader();
EditorGUILayout.Space(4);
_selectedTab = PSXEditorStyles.DrawButtonGroup(_tabNames, _selectedTab, 28);
EditorGUILayout.Space(4);
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
switch (_selectedTab)
{
case 0: // Dependencies
DrawToolchainSection();
EditorGUILayout.Space(2);
DrawNativeProjectSection();
break;
case 1: // Scenes
DrawScenesSection();
break;
case 2: // Build
DrawBuildSection();
break;
}
EditorGUILayout.EndScrollView();
}
// ═══════════════════════════════════════════════════════════════
// Header
// ═══════════════════════════════════════════════════════════════
private void DrawHeader()
{
EditorGUILayout.BeginHorizontal(PSXEditorStyles.ToolbarStyle);
GUILayout.Label("SplashEdit", PSXEditorStyles.WindowHeader);
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
// Status bar
{
string statusText;
Color statusColor;
if (!_hasMIPS)
{
statusText = "Setup required — install the MIPS toolchain to get started";
statusColor = PSXEditorStyles.Warning;
}
else if (!_hasNativeProject)
{
statusText = "Native project not found — clone or set path below";
statusColor = PSXEditorStyles.Warning;
}
else if (_isBuilding)
{
statusText = "Building...";
statusColor = PSXEditorStyles.Info;
}
else if (_isRunning)
{
statusText = "Running on " + (SplashSettings.Target == BuildTarget.Emulator ? "emulator" : "hardware");
statusColor = PSXEditorStyles.AccentGreen;
}
else
{
statusText = "Ready";
statusColor = PSXEditorStyles.Success;
}
EditorGUILayout.BeginHorizontal(PSXEditorStyles.InfoBox);
PSXEditorStyles.DrawStatusBadge(statusColor == PSXEditorStyles.Success ? "OK" :
statusColor == PSXEditorStyles.Warning ? "WARN" :
statusColor == PSXEditorStyles.Info ? "INFO" : "RUN", statusColor, 50);
GUILayout.Label(statusText, PSXEditorStyles.RichLabel);
EditorGUILayout.EndHorizontal();
}
}
// ═══════════════════════════════════════════════════════════════
// Native Project Section
// ═══════════════════════════════════════════════════════════════
private void DrawNativeProjectSection()
{
_showNativeProject = DrawSectionFoldout("Native Project (psxsplash)", _showNativeProject);
if (!_showNativeProject) return;
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
string currentPath = SplashBuildPaths.NativeSourceDir;
bool hasProject = !string.IsNullOrEmpty(currentPath) && Directory.Exists(currentPath);
// Status
EditorGUILayout.BeginHorizontal();
DrawStatusIcon(hasProject);
if (hasProject)
{
GUILayout.Label("Found at:", GUILayout.Width(60));
GUILayout.Label(TruncatePath(currentPath, 50), EditorStyles.miniLabel);
}
else
{
GUILayout.Label("Not found — clone from GitHub or set path manually", EditorStyles.miniLabel);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(6);
// ── Option 1: Auto-clone from GitHub ──
PSXEditorStyles.DrawSeparator(4, 4);
GUILayout.Label("Clone from GitHub", PSXEditorStyles.SectionHeader);
EditorGUILayout.BeginHorizontal();
GUILayout.Label(PSXSplashInstaller.RepoUrl, EditorStyles.miniLabel);
GUILayout.FlexibleSpace();
EditorGUI.BeginDisabledGroup(_isInstallingNative || PSXSplashInstaller.IsInstalled());
if (GUILayout.Button(PSXSplashInstaller.IsInstalled() ? "Installed" : "Clone", GUILayout.Width(80)))
{
CloneNativeProject();
}
EditorGUI.EndDisabledGroup();
EditorGUILayout.EndHorizontal();
if (_isInstallingNative)
{
GUILayout.Label(_nativeInstallStatus, PSXEditorStyles.InfoBox);
}
// If already cloned, show version management
if (PSXSplashInstaller.IsInstalled())
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Fetch Latest", EditorStyles.miniButton, GUILayout.Width(90)))
{
FetchNativeLatest();
}
if (GUILayout.Button("Open Folder", EditorStyles.miniButton, GUILayout.Width(90)))
{
EditorUtility.RevealInFinder(PSXSplashInstaller.FullInstallPath);
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.Space(6);
// ── Option 2: Manual path ──
PSXEditorStyles.DrawSeparator(4, 4);
GUILayout.Label("Or set path manually", PSXEditorStyles.SectionHeader);
EditorGUILayout.BeginHorizontal();
string newPath = EditorGUILayout.TextField(_manualNativePath);
if (newPath != _manualNativePath)
{
_manualNativePath = newPath;
}
if (GUILayout.Button("...", GUILayout.Width(30)))
{
string selected = EditorUtility.OpenFolderPanel("Select psxsplash Source Directory", _manualNativePath, "");
if (!string.IsNullOrEmpty(selected))
{
_manualNativePath = selected;
}
}
EditorGUILayout.EndHorizontal();
// Validate & apply the path
bool manualPathValid = !string.IsNullOrEmpty(_manualNativePath) &&
Directory.Exists(_manualNativePath) &&
File.Exists(Path.Combine(_manualNativePath, "Makefile"));
EditorGUILayout.BeginHorizontal();
if (!string.IsNullOrEmpty(_manualNativePath) && !manualPathValid)
{
GUILayout.Label("Invalid path. The directory must contain a Makefile.", PSXEditorStyles.InfoBox);
}
else if (manualPathValid && _manualNativePath != SplashSettings.NativeProjectPath)
{
if (GUILayout.Button("Apply", GUILayout.Width(60)))
{
SplashSettings.NativeProjectPath = _manualNativePath;
RefreshToolchainStatus();
Log($"Native project path set: {_manualNativePath}", LogType.Log);
}
}
EditorGUILayout.EndHorizontal();
if (manualPathValid && _manualNativePath == SplashSettings.NativeProjectPath)
{
var prevColor = GUI.contentColor;
GUI.contentColor = PSXEditorStyles.Success;
GUILayout.Label("✓ Path is set and valid", EditorStyles.miniLabel);
GUI.contentColor = prevColor;
}
EditorGUILayout.EndVertical();
}
// ═══════════════════════════════════════════════════════════════
// Toolchain Section
// ═══════════════════════════════════════════════════════════════
private void DrawToolchainSection()
{
_showToolchainSection = DrawSectionFoldout("Toolchain", _showToolchainSection);
if (!_showToolchainSection) return;
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
// MIPS Compiler
EditorGUILayout.BeginHorizontal();
DrawStatusIcon(_hasMIPS);
GUILayout.Label("MIPS Cross-Compiler", GUILayout.Width(160));
GUILayout.FlexibleSpace();
if (!_hasMIPS)
{
if (GUILayout.Button("Install", PSXEditorStyles.SecondaryButton, GUILayout.Width(70)))
InstallMIPS();
}
else
{
PSXEditorStyles.DrawStatusBadge("Ready", PSXEditorStyles.Success);
}
EditorGUILayout.EndHorizontal();
PSXEditorStyles.DrawSeparator(2, 2);
// GNU Make
EditorGUILayout.BeginHorizontal();
DrawStatusIcon(_hasMake);
GUILayout.Label("GNU Make", GUILayout.Width(160));
GUILayout.FlexibleSpace();
if (!_hasMake)
{
if (GUILayout.Button("Install", PSXEditorStyles.SecondaryButton, GUILayout.Width(70)))
InstallMake();
}
else
{
PSXEditorStyles.DrawStatusBadge("Ready", PSXEditorStyles.Success);
}
EditorGUILayout.EndHorizontal();
PSXEditorStyles.DrawSeparator(2, 2);
// PCSX-Redux
EditorGUILayout.BeginHorizontal();
DrawStatusIcon(_hasRedux);
GUILayout.Label("PCSX-Redux", GUILayout.Width(160));
GUILayout.FlexibleSpace();
if (!_hasRedux)
{
if (GUILayout.Button("Download", PSXEditorStyles.SecondaryButton, GUILayout.Width(80)))
DownloadRedux();
}
else
{
PSXEditorStyles.DrawStatusBadge(_reduxVersion, PSXEditorStyles.Success);
}
EditorGUILayout.EndHorizontal();
PSXEditorStyles.DrawSeparator(2, 2);
// psxavenc (audio encoder)
EditorGUILayout.BeginHorizontal();
DrawStatusIcon(_hasPsxavenc);
GUILayout.Label("psxavenc (Audio)", GUILayout.Width(160));
GUILayout.FlexibleSpace();
if (!_hasPsxavenc)
{
if (GUILayout.Button("Download", PSXEditorStyles.SecondaryButton, GUILayout.Width(80)))
DownloadPsxavenc();
}
else
{
PSXEditorStyles.DrawStatusBadge("Installed", PSXEditorStyles.Success);
}
EditorGUILayout.EndHorizontal();
PSXEditorStyles.DrawSeparator(2, 2);
// mkpsxiso (ISO builder)
EditorGUILayout.BeginHorizontal();
DrawStatusIcon(_hasMkpsxiso);
GUILayout.Label("mkpsxiso (ISO)", GUILayout.Width(160));
GUILayout.FlexibleSpace();
if (!_hasMkpsxiso)
{
if (GUILayout.Button("Download", PSXEditorStyles.SecondaryButton, GUILayout.Width(80)))
DownloadMkpsxiso();
}
else
{
PSXEditorStyles.DrawStatusBadge("Installed", PSXEditorStyles.Success);
}
EditorGUILayout.EndHorizontal();
// Refresh button
EditorGUILayout.Space(2);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Refresh", EditorStyles.miniButton, GUILayout.Width(60)))
RefreshToolchainStatus();
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
// ═══════════════════════════════════════════════════════════════
// Scenes Section
// ═══════════════════════════════════════════════════════════════
private void DrawScenesSection()
{
_showScenesSection = DrawSectionFoldout("Scenes", _showScenesSection);
if (!_showScenesSection) return;
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
if (_sceneList.Count == 0)
{
GUILayout.Label(
"No scenes added yet.\n" +
"Each scene needs a GameObject with a PSXSceneExporter component.\n" +
"Drag scene assets here, or use the buttons below to add them.",
PSXEditorStyles.InfoBox);
}
// Draw scene list
int removeIndex = -1;
int moveUp = -1;
int moveDown = -1;
for (int i = 0; i < _sceneList.Count; i++)
{
EditorGUILayout.BeginHorizontal();
// Index badge
GUILayout.Label($"[{i}]", EditorStyles.miniLabel, GUILayout.Width(24));
// Scene asset field
var newAsset = (SceneAsset)EditorGUILayout.ObjectField(
_sceneList[i].asset, typeof(SceneAsset), false);
if (newAsset != _sceneList[i].asset)
{
var entry = _sceneList[i];
entry.asset = newAsset;
if (newAsset != null)
{
entry.path = AssetDatabase.GetAssetPath(newAsset);
entry.name = newAsset.name;
}
_sceneList[i] = entry;
SaveSceneList();
}
// Move buttons
EditorGUI.BeginDisabledGroup(i == 0);
if (GUILayout.Button("▲", EditorStyles.miniButtonLeft, GUILayout.Width(22)))
moveUp = i;
EditorGUI.EndDisabledGroup();
EditorGUI.BeginDisabledGroup(i == _sceneList.Count - 1);
if (GUILayout.Button("▼", EditorStyles.miniButtonRight, GUILayout.Width(22)))
moveDown = i;
EditorGUI.EndDisabledGroup();
// Remove
if (GUILayout.Button("×", EditorStyles.miniButton, GUILayout.Width(20)))
removeIndex = i;
EditorGUILayout.EndHorizontal();
}
// Apply deferred operations
if (removeIndex >= 0)
{
_sceneList.RemoveAt(removeIndex);
SaveSceneList();
}
if (moveUp >= 1)
{
var temp = _sceneList[moveUp];
_sceneList[moveUp] = _sceneList[moveUp - 1];
_sceneList[moveUp - 1] = temp;
SaveSceneList();
}
if (moveDown >= 0 && moveDown < _sceneList.Count - 1)
{
var temp = _sceneList[moveDown];
_sceneList[moveDown] = _sceneList[moveDown + 1];
_sceneList[moveDown + 1] = temp;
SaveSceneList();
}
// Add scene buttons
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("+ Add Current Scene", EditorStyles.miniButton))
{
AddCurrentScene();
}
if (GUILayout.Button("+ Add Scene...", EditorStyles.miniButton))
{
string path = EditorUtility.OpenFilePanel("Select Scene", "Assets", "unity");
if (!string.IsNullOrEmpty(path))
{
// Convert to project-relative path
string projectPath = Application.dataPath;
if (path.StartsWith(projectPath))
path = "Assets" + path.Substring(projectPath.Length);
AddSceneByPath(path);
}
}
EditorGUILayout.EndHorizontal();
// Handle drag & drop
HandleSceneDragDrop();
EditorGUILayout.EndVertical();
}
// ═══════════════════════════════════════════════════════════════
// VRAM & Textures Section
// ═══════════════════════════════════════════════════════════════
private void DrawVRAMSection()
{
_showVRAMSection = DrawSectionFoldout("VRAM & Textures", _showVRAMSection);
if (!_showVRAMSection) return;
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
// Framebuffer: hardcoded 320x240, vertical, dual-buffered
GUILayout.Label("Framebuffer", PSXEditorStyles.SectionHeader);
GUILayout.Label("Resolution: 320x240 (dual-buffered, vertical layout)", PSXEditorStyles.InfoBox);
PSXEditorStyles.DrawSeparator(4, 4);
GUILayout.Label("Advanced Tools", PSXEditorStyles.SectionHeader);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Open VRAM Editor", PSXEditorStyles.PrimaryButton, GUILayout.Height(26)))
{
VRAMEditorWindow.ShowWindow();
}
if (GUILayout.Button("Quantized Preview", PSXEditorStyles.PrimaryButton, GUILayout.Height(26)))
{
QuantizedPreviewWindow.ShowWindow();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
// ═══════════════════════════════════════════════════════════════
// Build & Run Section
// ═══════════════════════════════════════════════════════════════
private void DrawBuildSection()
{
_showBuildSection = DrawSectionFoldout("Build && Run", _showBuildSection);
if (!_showBuildSection) return;
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
// Target & Mode
GUILayout.Label("Configuration", PSXEditorStyles.SectionHeader);
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Target:", GUILayout.Width(50));
SplashSettings.Target = (BuildTarget)EditorGUILayout.EnumPopup(SplashSettings.Target);
GUILayout.Label("Mode:", GUILayout.Width(40));
SplashSettings.Mode = (BuildMode)EditorGUILayout.EnumPopup(SplashSettings.Mode);
EditorGUILayout.EndHorizontal();
// 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);
SplashSettings.FpsOverlay = EditorGUILayout.Toggle(
new GUIContent("FPS Overlay", "Show an FPS counter at top-left during gameplay"),
SplashSettings.FpsOverlay);
// Serial port (only for Real Hardware)
if (SplashSettings.Target == BuildTarget.RealHardware)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Serial Port:", GUILayout.Width(80));
SplashSettings.SerialPort = EditorGUILayout.TextField(SplashSettings.SerialPort);
if (GUILayout.Button("Scan", EditorStyles.miniButton, GUILayout.Width(40)))
ScanSerialPorts();
EditorGUILayout.EndHorizontal();
}
// ISO settings (only for ISO build target)
if (SplashSettings.Target == BuildTarget.ISO)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Volume Label:", GUILayout.Width(80));
SplashSettings.ISOVolumeLabel = EditorGUILayout.TextField(SplashSettings.ISOVolumeLabel);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
GUILayout.Label("License File:", GUILayout.Width(80));
string licensePath = SplashSettings.LicenseFilePath;
string displayPath = string.IsNullOrEmpty(licensePath) ? "(none — homebrew)" : Path.GetFileName(licensePath);
GUILayout.Label(displayPath, EditorStyles.miniLabel, GUILayout.ExpandWidth(true));
if (GUILayout.Button("Browse", EditorStyles.miniButton, GUILayout.Width(60)))
{
string path = EditorUtility.OpenFilePanel(
"Select Sony License File", "", "dat");
if (!string.IsNullOrEmpty(path))
SplashSettings.LicenseFilePath = path;
}
if (!string.IsNullOrEmpty(licensePath) &&
GUILayout.Button("Clear", EditorStyles.miniButton, GUILayout.Width(40)))
{
SplashSettings.LicenseFilePath = "";
}
EditorGUILayout.EndHorizontal();
}
PSXEditorStyles.DrawSeparator(6, 6);
// Big Build & Run button
EditorGUI.BeginDisabledGroup(_isBuilding);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
var largeBuildButton = new GUIStyle(PSXEditorStyles.SuccessButton)
{
fontSize = 14,
fontStyle = FontStyle.Bold,
padding = new RectOffset(20, 20, 10, 10)
};
string buttonLabel = _isBuilding ? "Building..." :
(SplashSettings.Target == BuildTarget.ISO ? "BUILD" : "BUILD & RUN");
if (GUILayout.Button(buttonLabel, largeBuildButton, GUILayout.Width(200), GUILayout.Height(38)))
{
BuildAndRun();
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUI.EndDisabledGroup();
// Stop button (if running - emulator or hardware PCdrv host)
if (_isRunning)
{
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
string stopLabel = _emulatorProcess != null ? "STOP EMULATOR" : "STOP PCdrv HOST";
if (GUILayout.Button(stopLabel, PSXEditorStyles.DangerButton, GUILayout.Width(200), GUILayout.Height(26)))
{
StopAll();
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
// Export-only / Compile-only
PSXEditorStyles.DrawSeparator(4, 4);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Export Only", PSXEditorStyles.SecondaryButton, GUILayout.Width(100)))
{
ExportAllScenes();
}
if (GUILayout.Button("Compile Only", PSXEditorStyles.SecondaryButton, GUILayout.Width(100)))
{
CompileOnly();
}
GUILayout.FlexibleSpace();
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";
}
// ═══════════════════════════════════════════════════════════════
// Pipeline Actions
// ═══════════════════════════════════════════════════════════════
///
/// The main pipeline: Validate → Export all scenes → Compile → Launch.
///
public async void BuildAndRun()
{
if (_isBuilding) return;
if (UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().isDirty)
{
int choice = EditorUtility.DisplayDialogComplex(
"Unsaved Changes",
"The current scene has unsaved changes. Save before building?",
"Save and Build", // 0
"Cancel", // 1
"Build Without Saving" // 2
);
if (choice == 1) return; // Cancel
if (choice == 0) EditorSceneManager.SaveOpenScenes();
}
_isBuilding = true;
var console = EditorWindow.GetWindow();
console.titleContent = new GUIContent("PSX Console", EditorGUIUtility.IconContent("d_UnityEditor.ConsoleWindow").image);
console.minSize = new Vector2(400, 200);
console.Show();
try
{
Log("Validating toolchain...", LogType.Log);
if (!ValidateToolchain())
{
Log("Toolchain validation failed. Fix issues above.", LogType.Error);
return;
}
Log("Toolchain OK.", LogType.Log);
Log("Exporting scenes...", LogType.Log);
if (!ExportAllScenes())
{
Log("Export failed.", LogType.Error);
return;
}
Log($"Exported {_sceneList.Count} scene(s).", LogType.Log);
Log("Compiling native code...", LogType.Log);
EditorUtility.DisplayProgressBar("SplashEdit", "Compiling native code...", 0.6f);
if (!await CompileNativeAsync())
{
Log("Compilation failed. Check build log.", LogType.Error);
return;
}
Log("Compile succeeded.", LogType.Log);
Log("Launching...", LogType.Log);
Launch();
}
catch (Exception ex)
{
Log($"Pipeline error: {ex.Message}", LogType.Error);
Debug.LogException(ex);
}
finally
{
_isBuilding = false;
EditorUtility.ClearProgressBar();
Repaint();
}
}
// ───── Step 1: Validate ─────
private bool ValidateToolchain()
{
RefreshToolchainStatus();
if (!_hasMIPS)
{
Log("MIPS cross-compiler not found. Click Install in the Toolchain section.", LogType.Error);
return false;
}
if (!_hasMake)
{
Log("GNU Make not found. Click Install in the Toolchain section.", LogType.Error);
return false;
}
if (SplashSettings.Target == BuildTarget.Emulator && !_hasRedux)
{
Log("PCSX-Redux not found. Click Download in the Toolchain section.", LogType.Error);
return false;
}
if (SplashSettings.Target == BuildTarget.ISO && !_hasMkpsxiso)
{
Log("mkpsxiso not found. Click Download in the Toolchain section.", LogType.Error);
return false;
}
string nativeDir = SplashBuildPaths.NativeSourceDir;
if (string.IsNullOrEmpty(nativeDir) || !Directory.Exists(nativeDir))
{
Log("Native project directory not found. Set it in the Toolchain section.", LogType.Error);
return false;
}
if (_sceneList.Count == 0)
{
Log("No scenes in the scene list. Add at least one scene.", LogType.Error);
return false;
}
return true;
}
// ───── Step 2: Export ─────
///
/// Exports all scenes in the scene list to splashpack files in PSXBuild/.
///
public bool ExportAllScenes()
{
SplashBuildPaths.EnsureDirectories();
_loaderPackCache = new Dictionary();
_memoryReports.Clear();
// Save current scene
string currentScenePath = SceneManager.GetActiveScene().path;
bool success = true;
for (int i = 0; i < _sceneList.Count; i++)
{
var scene = _sceneList[i];
if (scene.asset == null)
{
Log($"Scene [{i}] is null, skipping.", LogType.Warning);
continue;
}
EditorUtility.DisplayProgressBar("SplashEdit Export",
$"Exporting scene {i + 1}/{_sceneList.Count}: {scene.name}",
(float)i / _sceneList.Count);
try
{
// Open the scene
EditorSceneManager.OpenScene(scene.path, OpenSceneMode.Single);
// Find the exporter
var exporter = UnityEngine.Object.FindFirstObjectByType();
if (exporter == null)
{
Log($"Scene '{scene.name}' has no PSXSceneExporter. Skipping.", LogType.Warning);
continue;
}
// 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)
{
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)
{
Log($"Error exporting '{scene.name}': {ex.Message}", LogType.Error);
success = false;
}
}
// Write manifest (simple binary: scene count + list of filenames)
WriteManifest();
EditorUtility.ClearProgressBar();
// Reopen orignal scene
if (!string.IsNullOrEmpty(currentScenePath))
{
EditorSceneManager.OpenScene(currentScenePath, OpenSceneMode.Single);
}
return success;
}
///
/// Cache of already-exported loader packs for deduplication.
/// Key = prefab asset GUID, Value = path of the written file.
/// If two scenes reference the same loading screen prefab, we copy the file
/// instead of regenerating it.
///
private Dictionary _loaderPackCache = new Dictionary();
private void ExportLoaderPack(GameObject prefab, string outputPath, int sceneIndex, string sceneName)
{
string prefabPath = AssetDatabase.GetAssetPath(prefab);
string guid = AssetDatabase.AssetPathToGUID(prefabPath);
// Dedup: if we already exported this exact prefab, just copy the file
if (!string.IsNullOrEmpty(guid) && _loaderPackCache.TryGetValue(guid, out string cachedPath))
{
if (File.Exists(cachedPath))
{
File.Copy(cachedPath, outputPath, true);
Log($"Loading screen for '{sceneName}' → {Path.GetFileName(outputPath)} (deduped from {Path.GetFileName(cachedPath)})", LogType.Log);
return;
}
}
// Need the PSXData resolution to pass to the writer
Vector2 resolution;
bool db, vl;
List pa;
DataStorage.LoadData(out resolution, out db, out vl, out pa);
// Instantiate the prefab temporarily so the components are live
// (GetComponentsInChildren needs active hierarchy)
GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
try
{
// Pack UI image textures into VRAM (same flow as PSXSceneExporter)
TextureAtlas[] atlases = null;
PSXUIImage[] uiImages = instance.GetComponentsInChildren(true);
if (uiImages != null && uiImages.Length > 0)
{
List uiTextures = new List();
foreach (PSXUIImage img in uiImages)
{
if (img.SourceTexture != null)
{
Utils.SetTextureImporterFormat(img.SourceTexture, true);
PSXTexture2D tex = PSXTexture2D.CreateFromTexture2D(img.SourceTexture, img.BitDepth);
tex.OriginalTexture = img.SourceTexture;
img.PackedTexture = tex;
uiTextures.Add(tex);
}
}
if (uiTextures.Count > 0)
{
(Rect buffer1, Rect buffer2) = Utils.BufferForResolution(resolution, vl);
List framebuffers = new List { buffer1 };
if (db) framebuffers.Add(buffer2);
VRAMPacker packer = new VRAMPacker(framebuffers, pa);
var packed = packer.PackTexturesIntoVRAM(new PSXObjectExporter[0], uiTextures);
atlases = packed.atlases;
}
}
// CollectCanvasFromPrefab reads PackedTexture VRAM coords (set by packer above)
bool ok = PSXLoaderPackWriter.Write(outputPath, instance, resolution, atlases,
(msg, type) => Log(msg, type));
if (ok)
{
Log($"Loading screen for '{sceneName}' → {Path.GetFileName(outputPath)}", LogType.Log);
if (!string.IsNullOrEmpty(guid))
_loaderPackCache[guid] = outputPath;
}
}
finally
{
UnityEngine.Object.DestroyImmediate(instance);
}
}
private void WriteManifest()
{
string manifestPath = SplashBuildPaths.ManifestPath;
using (var writer = new BinaryWriter(File.Open(manifestPath, FileMode.Create)))
{
// Magic "SM" for Scene Manifest
writer.Write((byte)'S');
writer.Write((byte)'M');
// Version
writer.Write((ushort)1);
// Scene count
writer.Write((uint)_sceneList.Count);
for (int i = 0; i < _sceneList.Count; i++)
{
string filename = Path.GetFileName(
SplashBuildPaths.GetSceneSplashpackPath(i, _sceneList[i].name));
byte[] nameBytes = System.Text.Encoding.UTF8.GetBytes(filename);
// Length-prefixed string
writer.Write((byte)nameBytes.Length);
writer.Write(nameBytes);
}
}
Log("Wrote scene manifest.", LogType.Log);
}
// ───── Step 3: Compile ─────
private async void CompileOnly()
{
if (_isBuilding) return;
_isBuilding = true;
Repaint();
try
{
Log("Compiling native code...", LogType.Log);
EditorUtility.DisplayProgressBar("SplashEdit", "Compiling native code...", 0.5f);
if (await CompileNativeAsync())
Log("Compile succeeded.", LogType.Log);
else
Log("Compilation failed. Check build log.", LogType.Error);
}
finally
{
_isBuilding = false;
EditorUtility.ClearProgressBar();
Repaint();
}
}
private async Task CompileNativeAsync()
{
string nativeDir = SplashBuildPaths.NativeSourceDir;
if (string.IsNullOrEmpty(nativeDir))
{
Log("Native project directory not set.", LogType.Error);
return false;
}
string buildArg = SplashSettings.Mode == BuildMode.Debug ? "BUILD=Debug" : "";
if (SplashSettings.Target == BuildTarget.ISO)
buildArg += " LOADER=cdrom";
if (SplashSettings.MemoryOverlay)
buildArg += " MEMOVERLAY=1";
if (SplashSettings.FpsOverlay)
buildArg += " FPSOVERLAY=1";
int jobCount = Math.Max(1, SystemInfo.processorCount - 1);
string cleanPrefix = SplashSettings.CleanBuild ? "make clean && " : "";
string makeCmd = $"{cleanPrefix}make all -j{jobCount} {buildArg}".Trim();
Log($"Running: {makeCmd}", LogType.Log);
var psi = new ProcessStartInfo
{
FileName = Application.platform == RuntimePlatform.WindowsEditor ? "cmd.exe" : "/bin/bash",
Arguments = Application.platform == RuntimePlatform.WindowsEditor
? $"/c \"cd /d \"{nativeDir}\" && {makeCmd}\""
: $"-c \"cd \\\"{nativeDir}\\\" && {makeCmd}\"",
WorkingDirectory = nativeDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
try
{
var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
var stdoutBuf = new System.Text.StringBuilder();
var stderrBuf = new System.Text.StringBuilder();
process.OutputDataReceived += (s, e) =>
{
if (e.Data != null) stdoutBuf.AppendLine(e.Data);
};
process.ErrorDataReceived += (s, e) =>
{
if (e.Data != null) stderrBuf.AppendLine(e.Data);
};
var tcs = new TaskCompletionSource();
process.Exited += (s, e) => tcs.TrySetResult(process.ExitCode);
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
int exitCode = await tcs.Task;
process.Dispose();
string stdout = stdoutBuf.ToString();
string stderr = stderrBuf.ToString();
foreach (string line in stdout.Split('\n'))
{
if (!string.IsNullOrWhiteSpace(line))
LogToPanel(line.Trim(), LogType.Log);
}
if (exitCode != 0)
{
foreach (string line in stderr.Split('\n'))
{
if (!string.IsNullOrWhiteSpace(line))
LogToPanel(line.Trim(), LogType.Error);
}
Log($"Make exited with code {exitCode}", LogType.Error);
File.WriteAllText(SplashBuildPaths.BuildLogPath,
$"=== STDOUT ===\n{stdout}\n=== STDERR ===\n{stderr}");
return false;
}
string exeSource = FindCompiledExe(nativeDir);
if (!string.IsNullOrEmpty(exeSource))
{
File.Copy(exeSource, SplashBuildPaths.CompiledExePath, true);
Log("Copied .ps-exe to PSXBuild/", LogType.Log);
}
else
{
Log("Warning: Could not find compiled .ps-exe", LogType.Warning);
}
return true;
}
catch (Exception ex)
{
Log($"Compile error: {ex.Message}", LogType.Error);
return false;
}
}
private string FindCompiledExe(string nativeDir)
{
// Look for .ps-exe files in the native dir
var files = Directory.GetFiles(nativeDir, "*.ps-exe", SearchOption.TopDirectoryOnly);
if (files.Length > 0)
return files[0];
// Also check common build output locations
foreach (string subdir in new[] { "build", "bin", "." })
{
string dir = Path.Combine(nativeDir, subdir);
if (Directory.Exists(dir))
{
files = Directory.GetFiles(dir, "*.ps-exe", SearchOption.TopDirectoryOnly);
if (files.Length > 0)
return files[0];
}
}
return null;
}
// ───── Step 4: Launch ─────
private void Launch()
{
switch (SplashSettings.Target)
{
case BuildTarget.Emulator:
LaunchEmulator();
break;
case BuildTarget.RealHardware:
LaunchToHardware();
break;
case BuildTarget.ISO:
BuildAndLaunchISO();
break;
}
}
private void LaunchEmulator()
{
string reduxPath = SplashSettings.PCSXReduxPath;
if (string.IsNullOrEmpty(reduxPath) || !File.Exists(reduxPath))
{
Log("PCSX-Redux binary not found.", LogType.Error);
return;
}
string exePath = SplashBuildPaths.CompiledExePath;
if (!File.Exists(exePath))
{
Log("Compiled .ps-exe not found in PSXBuild/.", LogType.Error);
return;
}
// Kill previous instance without clearing the console
StopAllQuiet();
string pcdrvBase = SplashBuildPaths.BuildOutputDir;
string args = $"-exe \"{exePath}\" -run -fastboot -pcdrv -pcdrvbase \"{pcdrvBase}\" -pad1type dualshock -stdout -interpreter";
Log($"Launching: {Path.GetFileName(reduxPath)} {args}", LogType.Log);
var psi = new ProcessStartInfo
{
FileName = reduxPath,
Arguments = args,
UseShellExecute = false,
// CreateNoWindow = true prevents pcsx-redux's -stdout AllocConsole() from
// stealing stdout away from our pipe. pcsx-redux is a GUI app and doesn't
// need a console window - it creates its own OpenGL/SDL window.
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
try
{
_emulatorProcess = Process.Start(psi);
_emulatorProcess.EnableRaisingEvents = true;
_emulatorProcess.Exited += (s, e) => {
EditorApplication.delayCall += () => {
_isRunning = false;
_emulatorProcess = null;
PSXConsoleWindow.Detach();
Repaint();
};
};
_isRunning = true;
Log("PCSX-Redux launched.", LogType.Log);
// Open the PSX Console window and attach to the process output
PSXConsoleWindow.Attach(_emulatorProcess);
}
catch (Exception ex)
{
Log($"Failed to launch emulator: {ex.Message}", LogType.Error);
}
}
private void LaunchToHardware()
{
string exePath = SplashBuildPaths.CompiledExePath;
if (!File.Exists(exePath))
{
Log("Compiled .ps-exe not found in PSXBuild/.", LogType.Error);
return;
}
string port = SplashSettings.SerialPort;
int baud = SplashSettings.SerialBaudRate;
// Stop any previous run (emulator or PCdrv) without clearing the console
StopAllQuiet();
// Upload the exe with debug hooks (DEBG → SEXE on the same port).
// DEBG installs kernel-resident break handlers BEFORE the exe auto-starts.
// The returned port stays open so PCDrv monitoring can begin immediately.
Log($"Uploading to {port}...", LogType.Log);
SerialPort serialPort = UniromUploader.UploadExeForPCdrv(port, baud, exePath,
msg => Log(msg, LogType.Log));
if (serialPort == null)
{
Log("Upload failed.", LogType.Error);
return;
}
// Start PCdrv host on the same open port — no re-open, no DEBG/CONT needed
try
{
_pcdrvHost = new PCdrvSerialHost(port, baud, SplashBuildPaths.BuildOutputDir,
msg => LogToPanel(msg, LogType.Log),
msg => PSXConsoleWindow.AddLine(msg));
_pcdrvHost.Start(serialPort);
_isRunning = true;
Log("PCdrv serial host started. Serving files to PS1.", LogType.Log);
}
catch (Exception ex)
{
Log($"PCdrv host error: {ex.Message}", LogType.Error);
try { serialPort.Close(); } catch { }
serialPort.Dispose();
}
}
// ───── ISO Build ─────
private void BuildAndLaunchISO()
{
if (!_hasMkpsxiso)
{
Log("mkpsxiso not installed. Click Download in the Toolchain section.", LogType.Error);
return;
}
string exePath = SplashBuildPaths.CompiledExePath;
if (!File.Exists(exePath))
{
Log("Compiled .ps-exe not found in PSXBuild/.", LogType.Error);
return;
}
// Ask user for output location
string defaultDir = SplashBuildPaths.BuildOutputDir;
string savePath = EditorUtility.SaveFilePanel(
"Save ISO Image", defaultDir, "psxsplash", "bin");
if (string.IsNullOrEmpty(savePath))
{
Log("ISO build cancelled.", LogType.Log);
return;
}
string outputBin = savePath;
string outputCue = Path.ChangeExtension(savePath, ".cue");
// Step 1: Generate SYSTEM.CNF
Log("Generating SYSTEM.CNF...", LogType.Log);
if (!GenerateSystemCnf())
{
Log("Failed to generate SYSTEM.CNF.", LogType.Error);
return;
}
// Step 2: Generate XML catalog for mkpsxiso
Log("Generating ISO catalog...", LogType.Log);
string xmlPath = GenerateISOCatalog(outputBin, outputCue);
if (string.IsNullOrEmpty(xmlPath))
{
Log("Failed to generate ISO catalog.", LogType.Error);
return;
}
// Step 3: Delete existing .bin/.cue — mkpsxiso won't overwrite them
try
{
if (File.Exists(outputBin)) File.Delete(outputBin);
if (File.Exists(outputCue)) File.Delete(outputCue);
}
catch (Exception ex)
{
Log($"Could not remove old ISO files: {ex.Message}", LogType.Error);
return;
}
// Step 4: Run mkpsxiso
Log("Building ISO image...", LogType.Log);
bool success = MkpsxisoDownloader.BuildISO(xmlPath, outputBin, outputCue,
msg => Log(msg, LogType.Log));
if (success)
{
long fileSize = new FileInfo(outputBin).Length;
Log($"ISO image written: {outputBin} ({fileSize:N0} bytes)", LogType.Log);
Log($"CUE sheet written: {outputCue}", LogType.Log);
// Offer to reveal in explorer
EditorUtility.RevealInFinder(outputBin);
}
else
{
Log("ISO build failed.", LogType.Error);
}
}
///
/// Derive the executable name on disc from the volume label.
/// Uppercase, no extension, trimmed to 12 characters (ISO9660 limit).
///
private static string GetISOExeName()
{
string label = SplashSettings.ISOVolumeLabel;
if (string.IsNullOrEmpty(label)) label = "PSXSPLASH";
// Uppercase, strip anything not A-Z / 0-9 / underscore
label = label.ToUpperInvariant();
var sb = new System.Text.StringBuilder(label.Length);
foreach (char c in label)
{
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')
sb.Append(c);
}
string name = sb.ToString();
if (name.Length == 0) name = "PSXSPLASH";
if (name.Length > 12) name = name.Substring(0, 12);
return name;
}
private bool GenerateSystemCnf()
{
try
{
string cnfPath = SplashBuildPaths.SystemCnfPath;
// The executable name on disc — no extension, max 12 chars
string exeName = GetISOExeName();
// SYSTEM.CNF content — the BIOS reads this to launch the executable.
// BOOT: path to the executable on disc (cdrom:\path;1)
// TCB: number of thread control blocks (4 is standard)
// EVENT: number of event control blocks (10 is standard)
// STACK: initial stack pointer address (top of RAM minus a small margin)
string content =
$"BOOT = cdrom:\\{exeName};1\r\n" +
"TCB = 4\r\n" +
"EVENT = 10\r\n" +
"STACK = 801FFF00\r\n";
File.WriteAllText(cnfPath, content, new System.Text.UTF8Encoding(false));
return true;
}
catch (Exception ex)
{
Log($"SYSTEM.CNF generation error: {ex.Message}", LogType.Error);
return false;
}
}
///
/// Generates the mkpsxiso XML catalog describing the ISO filesystem layout.
/// Includes SYSTEM.CNF, the executable, all splashpacks, loading packs, and manifest.
///
private string GenerateISOCatalog(string outputBin, string outputCue)
{
try
{
string xmlPath = SplashBuildPaths.ISOCatalogPath;
string buildDir = SplashBuildPaths.BuildOutputDir;
string volumeLabel = SplashSettings.ISOVolumeLabel;
if (string.IsNullOrEmpty(volumeLabel)) volumeLabel = "PSXSPLASH";
// Sanitize volume label (ISO9660: uppercase, max 31 chars)
volumeLabel = volumeLabel.ToUpperInvariant();
if (volumeLabel.Length > 31) volumeLabel = volumeLabel.Substring(0, 31);
var xml = new System.Text.StringBuilder();
xml.AppendLine("");
xml.AppendLine("");
xml.AppendLine(" ");
xml.AppendLine("");
File.WriteAllText(xmlPath, xml.ToString(), new System.Text.UTF8Encoding(false));
Log($"ISO catalog written: {xmlPath}", LogType.Log);
return xmlPath;
}
catch (Exception ex)
{
Log($"ISO catalog generation error: {ex.Message}", LogType.Error);
return null;
}
}
private static string EscapeXml(string s)
{
if (string.IsNullOrEmpty(s)) return "";
return s.Replace("&", "&").Replace("<", "<")
.Replace(">", ">").Replace("\"", """);
}
private void StopPCdrvHost()
{
if (_pcdrvHost != null)
{
_pcdrvHost.Dispose();
_pcdrvHost = null;
}
}
///
/// Stops everything (emulator, PCdrv host, console reader) — used by the STOP button.
///
private void StopAll()
{
PSXConsoleWindow.Detach();
StopEmulatorProcess();
StopPCdrvHost();
_isRunning = false;
Log("Stopped.", LogType.Log);
}
///
/// Stops emulator and PCdrv host without touching the console window.
/// Used before re-launching so the console keeps its history.
///
private void StopAllQuiet()
{
StopEmulatorProcess();
StopPCdrvHost();
_isRunning = false;
}
private void StopEmulatorProcess()
{
if (_emulatorProcess != null && !_emulatorProcess.HasExited)
{
try
{
_emulatorProcess.Kill();
_emulatorProcess.Dispose();
}
catch { }
}
_emulatorProcess = null;
}
// ═══════════════════════════════════════════════════════════════
// Toolchain Detection & Install
// ═══════════════════════════════════════════════════════════════
private void RefreshToolchainStatus()
{
_hasMIPS = ToolchainChecker.IsToolAvailable(
Application.platform == RuntimePlatform.WindowsEditor
? "mipsel-none-elf-gcc"
: "mipsel-linux-gnu-gcc");
_hasMake = ToolchainChecker.IsToolAvailable("make");
string reduxBin = SplashSettings.PCSXReduxPath;
_hasRedux = !string.IsNullOrEmpty(reduxBin) && File.Exists(reduxBin);
_reduxVersion = _hasRedux ? "Installed" : "";
_hasPsxavenc = PSXAudioConverter.IsInstalled();
_hasMkpsxiso = MkpsxisoDownloader.IsInstalled();
string nativeDir = SplashBuildPaths.NativeSourceDir;
_hasNativeProject = !string.IsNullOrEmpty(nativeDir) && Directory.Exists(nativeDir);
}
private async void InstallMIPS()
{
Log("Installing MIPS toolchain...", LogType.Log);
try
{
await ToolchainInstaller.InstallToolchain();
Log("MIPS toolchain installation started. You may need to restart.", LogType.Log);
}
catch (Exception ex)
{
Log($"MIPS install error: {ex.Message}", LogType.Error);
}
RefreshToolchainStatus();
Repaint();
}
private async void InstallMake()
{
Log("Installing GNU Make...", LogType.Log);
try
{
await ToolchainInstaller.InstallMake();
Log("GNU Make installation complete.", LogType.Log);
}
catch (Exception ex)
{
Log($"Make install error: {ex.Message}", LogType.Error);
}
RefreshToolchainStatus();
Repaint();
}
private async void DownloadRedux()
{
Log("Downloading PCSX-Redux...", LogType.Log);
bool success = await PCSXReduxDownloader.DownloadAndInstall(msg => Log(msg, LogType.Log));
if (success)
{
// Clear any custom path so it uses the auto-downloaded one
SplashSettings.PCSXReduxPath = "";
RefreshToolchainStatus();
Log("PCSX-Redux ready!", LogType.Log);
}
else
{
// Fall back to manual selection
Log("Auto-download failed. Select binary manually.", LogType.Warning);
string path = EditorUtility.OpenFilePanel("Select PCSX-Redux Binary", "",
Application.platform == RuntimePlatform.WindowsEditor ? "exe" : "");
if (!string.IsNullOrEmpty(path))
{
SplashSettings.PCSXReduxPath = path;
RefreshToolchainStatus();
Log($"PCSX-Redux set: {path}", LogType.Log);
}
}
Repaint();
}
private async void DownloadPsxavenc()
{
Log("Downloading psxavenc audio encoder...", LogType.Log);
bool success = await PSXAudioConverter.DownloadAndInstall(msg => Log(msg, LogType.Log));
if (success)
{
RefreshToolchainStatus();
Log("psxavenc ready!", LogType.Log);
}
else
{
Log("psxavenc download failed. Audio export will not work.", LogType.Error);
}
Repaint();
}
private async void DownloadMkpsxiso()
{
Log("Downloading mkpsxiso ISO builder...", LogType.Log);
bool success = await MkpsxisoDownloader.DownloadAndInstall(msg => Log(msg, LogType.Log));
if (success)
{
RefreshToolchainStatus();
Log("mkpsxiso ready!", LogType.Log);
}
else
{
Log("mkpsxiso download failed. ISO builds will not work.", LogType.Error);
}
Repaint();
}
private void ScanSerialPorts()
{
try
{
string[] ports = System.IO.Ports.SerialPort.GetPortNames();
if (ports.Length == 0)
{
Log("No serial ports found.", LogType.Warning);
}
else
{
Log($"Available ports: {string.Join(", ", ports)}", LogType.Log);
// Auto-select first port if current is empty
if (string.IsNullOrEmpty(SplashSettings.SerialPort))
SplashSettings.SerialPort = ports[0];
}
}
catch (Exception ex)
{
Log($"Error scanning ports: {ex.Message}", LogType.Error);
}
}
// ───── Native Project Clone/Fetch ─────
private async void CloneNativeProject()
{
_isInstallingNative = true;
_nativeInstallStatus = "Cloning psxsplash repository (this may take a minute)...";
Repaint();
Log("Cloning psxsplash native project from GitHub...", LogType.Log);
try
{
bool success = await PSXSplashInstaller.Install();
if (success)
{
Log("psxsplash cloned successfully!", LogType.Log);
_nativeInstallStatus = "";
RefreshToolchainStatus();
}
else
{
Log("Clone failed. Check console for errors.", LogType.Error);
_nativeInstallStatus = "Clone failed — check console for details.";
}
}
catch (Exception ex)
{
Log($"Clone error: {ex.Message}", LogType.Error);
_nativeInstallStatus = $"Error: {ex.Message}";
}
finally
{
_isInstallingNative = false;
Repaint();
}
}
private async void FetchNativeLatest()
{
Log("Fetching latest changes...", LogType.Log);
try
{
bool success = await PSXSplashInstaller.FetchLatestAsync();
if (success)
Log("Fetch complete. Use 'git pull' to apply updates.", LogType.Log);
else
Log("Fetch failed.", LogType.Warning);
}
catch (Exception ex)
{
Log($"Fetch error: {ex.Message}", LogType.Error);
}
Repaint();
}
// ═══════════════════════════════════════════════════════════════
// Scene List Persistence (EditorPrefs)
// ═══════════════════════════════════════════════════════════════
private void LoadSceneList()
{
_sceneList.Clear();
string prefix = "SplashEdit_" + Application.dataPath.GetHashCode().ToString("X8") + "_";
int count = EditorPrefs.GetInt(prefix + "SceneCount", 0);
for (int i = 0; i < count; i++)
{
string path = EditorPrefs.GetString(prefix + $"Scene_{i}", "");
if (string.IsNullOrEmpty(path)) continue;
var asset = AssetDatabase.LoadAssetAtPath(path);
_sceneList.Add(new SceneEntry
{
asset = asset,
path = path,
name = asset != null ? asset.name : Path.GetFileNameWithoutExtension(path)
});
}
}
private void SaveSceneList()
{
string prefix = "SplashEdit_" + Application.dataPath.GetHashCode().ToString("X8") + "_";
EditorPrefs.SetInt(prefix + "SceneCount", _sceneList.Count);
for (int i = 0; i < _sceneList.Count; i++)
{
EditorPrefs.SetString(prefix + $"Scene_{i}", _sceneList[i].path);
}
}
private void AddCurrentScene()
{
string scenePath = SceneManager.GetActiveScene().path;
if (string.IsNullOrEmpty(scenePath))
{
Log("Current scene is not saved. Save it first.", LogType.Warning);
return;
}
AddSceneByPath(scenePath);
}
private void AddSceneByPath(string path)
{
// Check for duplicates
if (_sceneList.Any(s => s.path == path))
{
Log($"Scene already in list: {path}", LogType.Warning);
return;
}
var asset = AssetDatabase.LoadAssetAtPath(path);
_sceneList.Add(new SceneEntry
{
asset = asset,
path = path,
name = asset != null ? asset.name : Path.GetFileNameWithoutExtension(path)
});
SaveSceneList();
Log($"Added scene: {Path.GetFileNameWithoutExtension(path)}", LogType.Log);
}
private void HandleSceneDragDrop()
{
Event evt = Event.current;
Rect dropArea = GUILayoutUtility.GetLastRect();
if (evt.type == EventType.DragUpdated || evt.type == EventType.DragPerform)
{
if (!dropArea.Contains(evt.mousePosition)) return;
bool hasScenes = DragAndDrop.objectReferences.Any(o => o is SceneAsset);
if (hasScenes)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if (evt.type == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
foreach (var obj in DragAndDrop.objectReferences)
{
if (obj is SceneAsset)
{
string path = AssetDatabase.GetAssetPath(obj);
AddSceneByPath(path);
}
}
}
evt.Use();
}
}
}
// ═══════════════════════════════════════════════════════════════
// Utilities
// ═══════════════════════════════════════════════════════════════
private static void Log(string message, LogType type)
{
bool isError = type == LogType.Error;
PSXConsoleWindow.AddLine(message, isError);
// Always log to Unity console as a fallback.
switch (type)
{
case LogType.Error:
Debug.LogError($"[SplashEdit] {message}");
break;
case LogType.Warning:
Debug.LogWarning($"[SplashEdit] {message}");
break;
default:
Debug.Log($"[SplashEdit] {message}");
break;
}
}
///
/// Writes make stdout/stderr to PSX Console and Unity console.
///
private static void LogToPanel(string message, LogType type)
{
PSXConsoleWindow.AddLine(message, type == LogType.Error);
Debug.Log($"[SplashEdit Build] {message}");
}
private bool DrawSectionFoldout(string title, bool isOpen)
{
EditorGUILayout.BeginHorizontal();
isOpen = EditorGUILayout.Foldout(isOpen, title, true, PSXEditorStyles.SectionHeader);
EditorGUILayout.EndHorizontal();
return isOpen;
}
private void DrawStatusIcon(bool ok)
{
var content = ok
? EditorGUIUtility.IconContent("d_greenLight")
: EditorGUIUtility.IconContent("d_redLight");
GUILayout.Label(content, GUILayout.Width(20), GUILayout.Height(20));
}
private string TruncatePath(string path, int maxLen)
{
if (path.Length <= maxLen) return path;
return "..." + path.Substring(path.Length - maxLen + 3);
}
}
}