From bb9d3594ffe349bd966da7eedaa0986c4fcb8667 Mon Sep 17 00:00:00 2001 From: Axel Kaotic Date: Sun, 12 Jul 2026 19:33:00 +0200 Subject: [PATCH 1/3] Improve PS5 (Prospero) SELF loader support in eboot.bin parsing Improved the SELF (Signed Executable and Linkable Format) loader to properly support both PS4 (Orbis) and PS5 (Prospero) eboot.bin files. --- src/SharpEmu.Core/Loader/SelfLoader.cs | 81 ++++++++++++-------------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/src/SharpEmu.Core/Loader/SelfLoader.cs b/src/SharpEmu.Core/Loader/SelfLoader.cs index 84299a9a..11f0f04b 100644 --- a/src/SharpEmu.Core/Loader/SelfLoader.cs +++ b/src/SharpEmu.Core/Loader/SelfLoader.cs @@ -17,7 +17,8 @@ namespace SharpEmu.Core.Loader; public sealed class SelfLoader : ISelfLoader { private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader"); - private const uint SelfMagic = 0x4F153D1D; + private const uint SelfOrbisMagic = 0x1D3D154F; + private const uint SelfProsperoMagic = 0xEEF51454; private const ulong SelfSegmentFlag = 0x800; private const int PageSize = 0x1000; private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL; @@ -82,8 +83,8 @@ public sealed class SelfLoader : ISelfLoader private static readonly IReadOnlyDictionary EmptyRuntimeSymbols = new Dictionary(StringComparer.Ordinal); private static readonly IReadOnlyList EmptyInitializerFunctions = Array.Empty(); - private static readonly int SelfHeaderSize = Unsafe.SizeOf(); - private static readonly int SelfSegmentSize = Unsafe.SizeOf(); + private static readonly int SelfHeaderSize = 32; // 0x20 bytes // Unsafe.SizeOf(); + private static readonly int SelfSegmentSize = 32; // 0x20 bytes per segment // Unsafe.SizeOf(); private static readonly int ProgramHeaderSize = Unsafe.SizeOf(); public SelfImage Load(ReadOnlySpan imageData, IVirtualMemory virtualMemory) @@ -323,12 +324,26 @@ private static LoadContext ParseLayout(ReadOnlySpan imageData) throw new InvalidDataException("Input image is too small to contain an ELF header."); } - if (imageData.Length >= sizeof(uint) && BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]) == SelfMagic) + if (imageData.Length >= sizeof(uint)) { + uint magic = BinaryPrimitives.ReadUInt32LittleEndian(imageData); + if (magic != SelfOrbisMagic && magic != SelfProsperoMagic) + { + throw new InvalidDataException($"Not a SELF file. Found magic 0x{magic:X8}"); + } + var selfHeader = ReadUnmanaged(imageData, 0); - if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22) + if (!selfHeader.IsValidSelf) + { + throw new InvalidDataException("SELF magic not recognized."); + } + if (selfHeader.SegmentCount == 0 || selfHeader.SegmentCount > 100) + { + throw new InvalidDataException($"Invalid segment count: {selfHeader.SegmentCount}"); + } + if (selfHeader.FileSize > (ulong)imageData.Length) { - throw new InvalidDataException("SELF header signature is not recognized."); + throw new InvalidDataException("SELF FileSize is larger than actual file."); } var segmentCount = selfHeader.SegmentCount; @@ -2354,44 +2369,22 @@ private enum SelfSegmentResolveStatus [StructLayout(LayoutKind.Sequential, Pack = 1)] private readonly struct SelfHeader { - private readonly byte _ident0; - private readonly byte _ident1; - private readonly byte _ident2; - private readonly byte _ident3; - private readonly byte _ident4; - private readonly byte _ident5; - private readonly byte _ident6; - private readonly byte _ident7; - private readonly byte _ident8; - private readonly byte _ident9; - private readonly byte _ident10; - private readonly byte _ident11; - private readonly ushort _size1; - private readonly ushort _size2; - private readonly ulong _fileSize; - private readonly ushort _segmentCount; - private readonly ushort _unknown; - private readonly uint _padding; - - public ushort SegmentCount => _segmentCount; - - public ushort Unknown => _unknown; - - public ulong FileSize => _fileSize; - - public bool HasKnownLayout => - _ident0 == 0x4F && - _ident1 == 0x15 && - _ident2 == 0x3D && - _ident3 == 0x1D && - _ident4 == 0x00 && - _ident5 == 0x01 && - _ident6 == 0x01 && - _ident7 == 0x12 && - _ident8 == 0x01 && - _ident9 == 0x01 && - _ident10 == 0x00 && - _ident11 == 0x00; + public readonly uint Magic; // 0x4F153D1D (PS4) or 0x5414F5EE (PS5) + public readonly byte Version; + public readonly byte Mode; + public readonly byte Endian; + public readonly byte Attrs; + public readonly uint KeyType; + public readonly ushort HeaderSize; + public readonly ushort MetaSize; + public readonly ulong FileSize; + public readonly ushort SegmentCount; + public readonly ushort Flags; + + public bool IsValidSelf => Magic == SelfOrbisMagic || Magic == SelfProsperoMagic; + + public bool IsProspero => Magic == SelfProsperoMagic; // PS5 + public bool IsOrbis => Magic == SelfOrbisMagic; // PS4 } [StructLayout(LayoutKind.Sequential, Pack = 1)] From 12962df48aa2c00ccea8da74bae13d449e0dfeb9 Mon Sep 17 00:00:00 2001 From: Axel Kaotic Date: Sun, 12 Jul 2026 21:48:23 +0200 Subject: [PATCH 2/3] Add detachable console window Add a Split button to detach the emulator console into a resizable window. The detached console keeps search, auto-scroll, copy, and clear controls. The main console hides while detached and restores when the split window closes if it was previously open. --- src/SharpEmu.GUI/ConsoleWindow.cs | 150 +++++++++++++++++++++++++++ src/SharpEmu.GUI/MainWindow.axaml | 12 ++- src/SharpEmu.GUI/MainWindow.axaml.cs | 29 +++++- 3 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 src/SharpEmu.GUI/ConsoleWindow.cs diff --git a/src/SharpEmu.GUI/ConsoleWindow.cs b/src/SharpEmu.GUI/ConsoleWindow.cs new file mode 100644 index 00000000..127cbe79 --- /dev/null +++ b/src/SharpEmu.GUI/ConsoleWindow.cs @@ -0,0 +1,150 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Threading; +using System.Collections.Specialized; + +namespace SharpEmu.GUI; + +public sealed class ConsoleWindow : Window +{ + private readonly AvaloniaList _sourceLines; + private readonly AvaloniaList _visibleLines = new(); + private readonly ListBox _list; + private readonly TextBox _searchBox; + private readonly CheckBox _autoScrollCheck; + + public ConsoleWindow( + AvaloniaList lines, + Action clear, + bool autoScroll) + { + _sourceLines = lines; + Title = "SharpEmu Console"; + Width = 980; + Height = 620; + MinWidth = 520; + MinHeight = 320; + Background = new SolidColorBrush(Color.Parse("#0D1017")); + Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico"))); + + _searchBox = new TextBox { Watermark = "Search...", Width = 320, Margin = new Thickness(0, 0, 12, 0) }; + _autoScrollCheck = new CheckBox + { + Content = "Auto-scroll", + IsChecked = autoScroll, + FontSize = 12, + Margin = new Thickness(0, 0, 12, 0), + VerticalAlignment = VerticalAlignment.Center, + }; + var copyButton = new Button + { + Classes = { "ghost" }, + Content = "Copy", + Padding = new Thickness(10, 4), + Margin = new Thickness(0, 0, 8, 0), + }; + var clearButton = new Button { Classes = { "ghost" }, Content = "Clear", Padding = new Thickness(10, 4) }; + copyButton.Click += async (_, _) => await CopyAsync(); + clearButton.Click += (_, _) => clear(); + _searchBox.TextChanged += (_, _) => RefreshVisibleLines(); + + _list = new ListBox + { + Classes = { "console" }, + ItemsSource = _visibleLines, + BorderThickness = new Thickness(1), + BorderBrush = new SolidColorBrush(Color.Parse("#232B3A")), + ItemTemplate = new FuncDataTemplate((_, _) => + { + var text = new TextBlock { TextWrapping = TextWrapping.NoWrap }; + text.Bind(TextBlock.TextProperty, new Binding(nameof(LogLine.Text))); + text.Bind(TextBlock.ForegroundProperty, new Binding(nameof(LogLine.Brush))); + return text; + }), + }; + + Content = new Grid + { + Margin = new Thickness(12), + RowDefinitions = new RowDefinitions("Auto,*"), + Children = + { + new Grid + { + Margin = new Thickness(0, 0, 0, 8), + ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto"), + Children = + { + new TextBlock + { + Classes = { "sectionTitle" }, + Text = "CONSOLE", + VerticalAlignment = VerticalAlignment.Center, + }, + _searchBox.WithGridColumn(1), + _autoScrollCheck.WithGridColumn(2), + copyButton.WithGridColumn(3), + clearButton.WithGridColumn(4), + }, + }, + _list.WithGridRow(1), + }, + }; + + lines.CollectionChanged += OnLinesChanged; + Closed += (_, _) => lines.CollectionChanged -= OnLinesChanged; + RefreshVisibleLines(); + } + + private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + RefreshVisibleLines(); + if (_autoScrollCheck.IsChecked == true) + { + Dispatcher.UIThread.Post(() => (_list.Scroll as ScrollViewer)?.ScrollToEnd()); + } + } + + private void RefreshVisibleLines() + { + var query = _searchBox.Text ?? string.Empty; + _visibleLines.Clear(); + _visibleLines.AddRange(string.IsNullOrWhiteSpace(query) + ? _sourceLines + : _sourceLines.Where(line => line.Text.Contains(query, StringComparison.OrdinalIgnoreCase))); + } + + private async Task CopyAsync() + { + if (_visibleLines.Count == 0 || Clipboard is null) + { + return; + } + + await Clipboard.SetTextAsync(string.Join(Environment.NewLine, _visibleLines.Select(line => line.Text))); + } +} + +file static class GridExtensions +{ + public static T WithGridColumn(this T control, int column) where T : Control + { + Grid.SetColumn(control, column); + return control; + } + + public static T WithGridRow(this T control, int row) where T : Control + { + Grid.SetRow(control, row); + return control; + } +} diff --git a/src/SharpEmu.GUI/MainWindow.axaml b/src/SharpEmu.GUI/MainWindow.axaml index 53b68712..555f3f51 100644 --- a/src/SharpEmu.GUI/MainWindow.axaml +++ b/src/SharpEmu.GUI/MainWindow.axaml @@ -160,15 +160,17 @@ SPDX-License-Identifier: GPL-2.0-or-later - + - + -