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

@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
@@ -46,6 +47,7 @@ namespace SplashEdit.EditorCode
private bool _hasRedux;
private bool _hasNativeProject;
private bool _hasPsxavenc;
private bool _hasMkpsxiso;
private string _reduxVersion = "";
// ───── Native project installer ─────
@@ -486,6 +488,22 @@ namespace SplashEdit.EditorCode
}
EditorGUILayout.EndHorizontal();
// mkpsxiso (ISO builder)
EditorGUILayout.BeginHorizontal();
DrawStatusIcon(_hasMkpsxiso);
GUILayout.Label("mkpsxiso (ISO)", GUILayout.Width(160));
GUILayout.FlexibleSpace();
if (!_hasMkpsxiso)
{
if (GUILayout.Button("Download", GUILayout.Width(80)))
DownloadMkpsxiso();
}
else
{
GUILayout.Label("Installed", EditorStyles.miniLabel);
}
EditorGUILayout.EndHorizontal();
// Refresh button
EditorGUILayout.Space(2);
EditorGUILayout.BeginHorizontal();
@@ -653,14 +671,11 @@ namespace SplashEdit.EditorCode
EditorGUILayout.Space(4);
// GTE Scaling
EditorGUILayout.LabelField("Export Settings", EditorStyles.boldLabel);
SplashSettings.DefaultGTEScaling = EditorGUILayout.FloatField("Default GTE Scaling", SplashSettings.DefaultGTEScaling);
SplashSettings.AutoValidateOnExport = EditorGUILayout.Toggle("Auto-Validate on Export", SplashSettings.AutoValidateOnExport);
EditorGUILayout.Space(6);
// Open dedicated VRAM windows
EditorGUILayout.LabelField("Advanced Tools", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Open VRAM Editor", GUILayout.Height(24)))
@@ -673,11 +688,6 @@ namespace SplashEdit.EditorCode
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("Open Scene Validator", EditorStyles.miniButton))
{
PSXSceneValidatorWindow.ShowWindow();
}
EditorGUILayout.EndVertical();
}
@@ -711,6 +721,34 @@ namespace SplashEdit.EditorCode
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();
}
EditorGUILayout.Space(8);
// Big Build & Run button
@@ -759,7 +797,7 @@ namespace SplashEdit.EditorCode
}
if (GUILayout.Button("Compile Only", EditorStyles.miniButton, GUILayout.Width(100)))
{
CompileNative();
CompileOnly();
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
@@ -774,12 +812,11 @@ namespace SplashEdit.EditorCode
/// <summary>
/// The main pipeline: Validate → Export all scenes → Compile → Launch.
/// </summary>
public void BuildAndRun()
public async void BuildAndRun()
{
if (_isBuilding) return;
_isBuilding = true;
// Open the PSX Console so build output is visible immediately
var console = EditorWindow.GetWindow<PSXConsoleWindow>();
console.titleContent = new GUIContent("PSX Console", EditorGUIUtility.IconContent("d_UnityEditor.ConsoleWindow").image);
console.minSize = new Vector2(400, 200);
@@ -787,7 +824,6 @@ namespace SplashEdit.EditorCode
try
{
// Step 1: Validate
Log("Validating toolchain...", LogType.Log);
if (!ValidateToolchain())
{
@@ -796,7 +832,6 @@ namespace SplashEdit.EditorCode
}
Log("Toolchain OK.", LogType.Log);
// Step 2: Export all scenes
Log("Exporting scenes...", LogType.Log);
if (!ExportAllScenes())
{
@@ -805,16 +840,15 @@ namespace SplashEdit.EditorCode
}
Log($"Exported {_sceneList.Count} scene(s).", LogType.Log);
// Step 3: Compile native
Log("Compiling native code...", LogType.Log);
if (!CompileNative())
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);
// Step 4: Launch
Log("Launching...", LogType.Log);
Launch();
}
@@ -852,6 +886,11 @@ namespace SplashEdit.EditorCode
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))
@@ -902,7 +941,7 @@ namespace SplashEdit.EditorCode
EditorSceneManager.OpenScene(scene.path, OpenSceneMode.Single);
// Find the exporter
var exporter = UnityEngine.Object.FindObjectOfType<PSXSceneExporter>();
var exporter = UnityEngine.Object.FindFirstObjectByType<PSXSceneExporter>();
if (exporter == null)
{
Log($"Scene '{scene.name}' has no PSXSceneExporter. Skipping.", LogType.Warning);
@@ -1051,10 +1090,29 @@ namespace SplashEdit.EditorCode
// ───── Step 3: Compile ─────
/// <summary>
/// Runs make in the native project directory.
/// </summary>
public bool CompileNative()
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<bool> CompileNativeAsync()
{
string nativeDir = SplashBuildPaths.NativeSourceDir;
if (string.IsNullOrEmpty(nativeDir))
@@ -1064,9 +1122,11 @@ namespace SplashEdit.EditorCode
}
string buildArg = SplashSettings.Mode == BuildMode.Debug ? "BUILD=Debug" : "";
// Run clean first, THEN build — "make clean all -jN" races clean vs build in sub-makes
string makeCmd = $"make clean && make all -j{SystemInfo.processorCount} {buildArg}".Trim();
if (SplashSettings.Target == BuildTarget.ISO)
buildArg += " LOADER=cdrom";
string makeCmd = $"make clean && make all -j{SystemInfo.processorCount} {buildArg}".Trim();
Log($"Running: {makeCmd}", LogType.Log);
var psi = new ProcessStartInfo
@@ -1084,45 +1144,57 @@ namespace SplashEdit.EditorCode
try
{
var process = Process.Start(psi);
string stdout = process.StandardOutput.ReadToEnd();
string stderr = process.StandardError.ReadToEnd();
process.WaitForExit();
var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
var stdoutBuf = new System.Text.StringBuilder();
var stderrBuf = new System.Text.StringBuilder();
// Log output to panel only (no Unity console spam)
if (!string.IsNullOrEmpty(stdout))
process.OutputDataReceived += (s, e) =>
{
foreach (string line in stdout.Split('\n'))
{
if (!string.IsNullOrWhiteSpace(line))
LogToPanel(line.Trim(), LogType.Log);
}
if (e.Data != null) stdoutBuf.AppendLine(e.Data);
};
process.ErrorDataReceived += (s, e) =>
{
if (e.Data != null) stderrBuf.AppendLine(e.Data);
};
var tcs = new TaskCompletionSource<int>();
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 (process.ExitCode != 0)
if (exitCode != 0)
{
if (!string.IsNullOrEmpty(stderr))
foreach (string line in stderr.Split('\n'))
{
foreach (string line in stderr.Split('\n'))
{
if (!string.IsNullOrWhiteSpace(line))
LogToPanel(line.Trim(), LogType.Error);
}
if (!string.IsNullOrWhiteSpace(line))
LogToPanel(line.Trim(), LogType.Error);
}
Log($"Make exited with code {process.ExitCode}", LogType.Error);
Log($"Make exited with code {exitCode}", LogType.Error);
// Write build log file
File.WriteAllText(SplashBuildPaths.BuildLogPath,
$"=== STDOUT ===\n{stdout}\n=== STDERR ===\n{stderr}");
return false;
}
// Copy the compiled exe to PSXBuild/
string exeSource = FindCompiledExe(nativeDir);
if (!string.IsNullOrEmpty(exeSource))
{
File.Copy(exeSource, SplashBuildPaths.CompiledExePath, true);
Log($"Copied .ps-exe to PSXBuild/", LogType.Log);
Log("Copied .ps-exe to PSXBuild/", LogType.Log);
}
else
{
@@ -1172,7 +1244,7 @@ namespace SplashEdit.EditorCode
LaunchToHardware();
break;
case BuildTarget.ISO:
Log("ISO build not yet implemented.", LogType.Warning);
BuildAndLaunchISO();
break;
}
}
@@ -1274,6 +1346,235 @@ namespace SplashEdit.EditorCode
}
}
// ───── 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);
}
}
/// <summary>
/// Derive the executable name on disc from the volume label.
/// Uppercase, no extension, trimmed to 12 characters (ISO9660 limit).
/// </summary>
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;
}
}
/// <summary>
/// Generates the mkpsxiso XML catalog describing the ISO filesystem layout.
/// Includes SYSTEM.CNF, the executable, all splashpacks, loading packs, and manifest.
/// </summary>
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 version=\"1.0\" encoding=\"UTF-8\"?>");
xml.AppendLine("<iso_project image_name=\"psxsplash.bin\" cue_sheet=\"psxsplash.cue\">");
xml.AppendLine(" <track type=\"data\">");
xml.AppendLine(" <identifiers");
xml.AppendLine(" system=\"PLAYSTATION\"");
xml.AppendLine(" application=\"PLAYSTATION\"");
xml.AppendLine($" volume=\"{EscapeXml(volumeLabel)}\"");
xml.AppendLine($" volume_set=\"{EscapeXml(volumeLabel)}\"");
xml.AppendLine(" publisher=\"SPLASHEDIT\"");
xml.AppendLine(" data_preparer=\"MKPSXISO\"");
xml.AppendLine(" />");
// License file (optional)
string licensePath = SplashSettings.LicenseFilePath;
if (!string.IsNullOrEmpty(licensePath) && File.Exists(licensePath))
{
xml.AppendLine($" <license file=\"{EscapeXml(licensePath)}\"/>");
}
xml.AppendLine(" <directory_tree>");
// SYSTEM.CNF — must be first for BIOS to find it
string cnfPath = SplashBuildPaths.SystemCnfPath;
xml.AppendLine($" <file name=\"SYSTEM.CNF\" source=\"{EscapeXml(cnfPath)}\"/>");
// The executable — renamed to match what SYSTEM.CNF points to
string exePath = SplashBuildPaths.CompiledExePath;
string isoExeName = GetISOExeName();
xml.AppendLine($" <file name=\"{isoExeName}\" source=\"{EscapeXml(exePath)}\"/>");
// Manifest
string manifestPath = SplashBuildPaths.ManifestPath;
if (File.Exists(manifestPath))
{
xml.AppendLine($" <file name=\"MANIFEST.BIN\" source=\"{EscapeXml(manifestPath)}\"/>");
}
// Scene splashpacks and loading packs
for (int i = 0; i < _sceneList.Count; i++)
{
string splashpack = SplashBuildPaths.GetSceneSplashpackPath(i, _sceneList[i].name);
if (File.Exists(splashpack))
{
string isoName = $"SCENE_{i}.SPK";
xml.AppendLine($" <file name=\"{isoName}\" source=\"{EscapeXml(splashpack)}\"/>");
}
string loadingPack = SplashBuildPaths.GetSceneLoaderPackPath(i, _sceneList[i].name);
if (File.Exists(loadingPack))
{
string isoName = $"SCENE_{i}.LDG";
xml.AppendLine($" <file name=\"{isoName}\" source=\"{EscapeXml(loadingPack)}\"/>");
}
}
// Trailing dummy sectors to prevent drive runaway
xml.AppendLine(" <dummy sectors=\"128\"/>");
xml.AppendLine(" </directory_tree>");
xml.AppendLine(" </track>");
xml.AppendLine("</iso_project>");
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("&", "&amp;").Replace("<", "&lt;")
.Replace(">", "&gt;").Replace("\"", "&quot;");
}
private void StopPCdrvHost()
{
if (_pcdrvHost != null)
@@ -1339,6 +1640,8 @@ namespace SplashEdit.EditorCode
_hasPsxavenc = PSXAudioConverter.IsInstalled();
_hasMkpsxiso = MkpsxisoDownloader.IsInstalled();
string nativeDir = SplashBuildPaths.NativeSourceDir;
_hasNativeProject = !string.IsNullOrEmpty(nativeDir) && Directory.Exists(nativeDir);
}
@@ -1418,6 +1721,22 @@ namespace SplashEdit.EditorCode
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