mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 12:48:39 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c09b170b6 |
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 142 KiB |
@@ -32,18 +32,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<DebugType>none</DebugType>
|
<DebugType>none</DebugType>
|
||||||
<DebugSymbols>false</DebugSymbols>
|
<DebugSymbols>false</DebugSymbols>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Build provenance surfaced by SharpEmu.Logging.BuildInfo as a banner at the
|
|
||||||
top of the log. Populated from GitHub Actions environment variables when
|
|
||||||
building in CI; empty locally. -->
|
|
||||||
<ItemGroup>
|
|
||||||
<AssemblyMetadata Include="SharpEmu.BuildConfiguration" Value="$(Configuration)" />
|
|
||||||
<AssemblyMetadata Condition="'$(GITHUB_SHA)' != ''" Include="SharpEmu.BuildSha" Value="$(GITHUB_SHA)" />
|
|
||||||
<AssemblyMetadata Condition="'$(GITHUB_REF_NAME)' != ''" Include="SharpEmu.BuildBranch" Value="$(GITHUB_REF_NAME)" />
|
|
||||||
<AssemblyMetadata Condition="'$(GITHUB_REF)' != ''" Include="SharpEmu.BuildRef" Value="$(GITHUB_REF)" />
|
|
||||||
<AssemblyMetadata Condition="'$(GITHUB_EVENT_NAME)' != ''" Include="SharpEmu.BuildEventName" Value="$(GITHUB_EVENT_NAME)" />
|
|
||||||
<AssemblyMetadata Condition="'$(GITHUB_REPOSITORY)' != ''" Include="SharpEmu.BuildRepository" Value="$(GITHUB_REPOSITORY)" />
|
|
||||||
<AssemblyMetadata Condition="'$(GITHUB_SERVER_URL)' != ''" Include="SharpEmu.BuildServerUrl" Value="$(GITHUB_SERVER_URL)" />
|
|
||||||
<AssemblyMetadata Condition="'$(GITHUB_RUN_ID)' != ''" Include="SharpEmu.BuildRunId" Value="$(GITHUB_RUN_ID)" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2018 Alex Barney
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -15,9 +15,3 @@ path = [
|
|||||||
precedence = "aggregate"
|
precedence = "aggregate"
|
||||||
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
||||||
SPDX-License-Identifier = "GPL-2.0-or-later"
|
SPDX-License-Identifier = "GPL-2.0-or-later"
|
||||||
|
|
||||||
[[annotations]]
|
|
||||||
path = "src/SharpEmu.GUI/Atrac9/**"
|
|
||||||
precedence = "aggregate"
|
|
||||||
SPDX-FileCopyrightText = "2018 Alex Barney"
|
|
||||||
SPDX-License-Identifier = "MIT"
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "10.0.103",
|
"version": "10.0.103",
|
||||||
"rollForward": "disable"
|
"rollForward": "latestFeature"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,18 +36,6 @@ internal static partial class Program
|
|||||||
|
|
||||||
[STAThread]
|
[STAThread]
|
||||||
private static int Main(string[] args)
|
private static int Main(string[] args)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Run(args);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
SharpEmuLog.Shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int Run(string[] args)
|
|
||||||
{
|
{
|
||||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||||
if (args.Length == 0 && !isMitigatedChild)
|
if (args.Length == 0 && !isMitigatedChild)
|
||||||
@@ -60,7 +48,6 @@ internal static partial class Program
|
|||||||
// The executable uses the GUI subsystem, so CLI mode has to connect
|
// The executable uses the GUI subsystem, so CLI mode has to connect
|
||||||
// itself to a console before the first write.
|
// itself to a console before the first write.
|
||||||
EnsureCliConsole();
|
EnsureCliConsole();
|
||||||
UseUtf8ConsoleOutput();
|
|
||||||
|
|
||||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||||
|
|
||||||
@@ -77,11 +64,9 @@ internal static partial class Program
|
|||||||
|
|
||||||
SharpEmuLog.MinimumLevel = logLevel;
|
SharpEmuLog.MinimumLevel = logLevel;
|
||||||
|
|
||||||
Log.Info(BuildInfo.Banner);
|
|
||||||
|
|
||||||
ebootPath = Path.GetFullPath(ebootPath);
|
ebootPath = Path.GetFullPath(ebootPath);
|
||||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||||
|
|
||||||
if (!File.Exists(ebootPath))
|
if (!File.Exists(ebootPath))
|
||||||
{
|
{
|
||||||
Log.Error($"EBOOT file was not found: {ebootPath}");
|
Log.Error($"EBOOT file was not found: {ebootPath}");
|
||||||
@@ -163,48 +148,6 @@ internal static partial class Program
|
|||||||
RebindStdHandleToConsole(STD_ERROR_HANDLE);
|
RebindStdHandleToConsole(STD_ERROR_HANDLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Makes console writes UTF-8 so the GUI's pipe reader (and any modern
|
|
||||||
/// terminal) decodes non-ASCII text correctly. Without this, redirected
|
|
||||||
/// output falls back to the OS ANSI code page and characters like "—"
|
|
||||||
/// arrive mangled.
|
|
||||||
/// </summary>
|
|
||||||
private static void UseUtf8ConsoleOutput()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Also recreates the redirected Console.Out/Error writers with
|
|
||||||
// the new encoding.
|
|
||||||
Console.OutputEncoding = Encoding.UTF8;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// No attached console (GUI-subsystem child with piped output):
|
|
||||||
// wrap the raw handles instead.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Console.IsOutputRedirected)
|
|
||||||
{
|
|
||||||
Console.SetOut(new StreamWriter(
|
|
||||||
Console.OpenStandardOutput(),
|
|
||||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
|
|
||||||
{
|
|
||||||
AutoFlush = true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Console.IsErrorRedirected)
|
|
||||||
{
|
|
||||||
Console.SetError(new StreamWriter(
|
|
||||||
Console.OpenStandardError(),
|
|
||||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
|
|
||||||
{
|
|
||||||
AutoFlush = true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void RebindStdHandleToConsole(int stdHandle)
|
private static void RebindStdHandleToConsole(int stdHandle)
|
||||||
{
|
{
|
||||||
if (IsHandleValid(GetStdHandle(stdHandle)) || GetConsoleWindow() == 0)
|
if (IsHandleValid(GetStdHandle(stdHandle)) || GetConsoleWindow() == 0)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
"net10.0": {
|
"net10.0": {
|
||||||
"Microsoft.NET.ILLink.Tasks": {
|
"Microsoft.NET.ILLink.Tasks": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[10.0.3, )",
|
"requested": "[10.0.9, )",
|
||||||
"resolved": "10.0.3",
|
"resolved": "10.0.9",
|
||||||
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
"contentHash": "4Iw41e2h7I4t70SJcX2GCmbyKJIlA273Cfm9RJMM050/3VBejGAG1KcthP5Z2L6SQcbfbf6BhNWO26+ZG+GzMg=="
|
||||||
},
|
},
|
||||||
"Avalonia.Angle.Windows.Natives": {
|
"Avalonia.Angle.Windows.Natives": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
@@ -211,7 +211,6 @@
|
|||||||
"Avalonia.Desktop": "[11.3.18, )",
|
"Avalonia.Desktop": "[11.3.18, )",
|
||||||
"Avalonia.Fonts.Inter": "[11.3.18, )",
|
"Avalonia.Fonts.Inter": "[11.3.18, )",
|
||||||
"Avalonia.Themes.Fluent": "[11.3.18, )",
|
"Avalonia.Themes.Fluent": "[11.3.18, )",
|
||||||
"SharpEmu.Logging": "[1.0.0, )",
|
|
||||||
"Tmds.DBus.Protocol": "[0.21.3, )"
|
"Tmds.DBus.Protocol": "[0.21.3, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,14 +7,11 @@ using SharpEmu.Core.Cpu.Native;
|
|||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Logging;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu;
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
|
|
||||||
|
|
||||||
private enum EntryFrameKind
|
private enum EntryFrameKind
|
||||||
{
|
{
|
||||||
ProcessEntry,
|
ProcessEntry,
|
||||||
@@ -83,8 +80,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
string processImageName = "eboot.bin",
|
string processImageName = "eboot.bin",
|
||||||
CpuExecutionOptions executionOptions = default)
|
CpuExecutionOptions executionOptions = default)
|
||||||
{
|
{
|
||||||
Log.Debug("=== DispatchEntry START ===");
|
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
|
||||||
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
|
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -92,7 +89,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
|
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,8 +103,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
string moduleName = "module",
|
string moduleName = "module",
|
||||||
CpuExecutionOptions executionOptions = default)
|
CpuExecutionOptions executionOptions = default)
|
||||||
{
|
{
|
||||||
Log.Debug("=== DispatchModuleInitializer START ===");
|
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
|
||||||
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -121,7 +119,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
|
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,7 +134,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
CpuExecutionOptions executionOptions = default,
|
CpuExecutionOptions executionOptions = default,
|
||||||
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
||||||
{
|
{
|
||||||
Log.Debug("DispatchEntryCore STARTING...");
|
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
|
||||||
|
|
||||||
LastEntryPoint = entryPoint;
|
LastEntryPoint = entryPoint;
|
||||||
LastTrapInfo = null;
|
LastTrapInfo = null;
|
||||||
@@ -307,7 +306,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
LastMilestoneLog,
|
LastMilestoneLog,
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
$"CpuEngine native-only failed: {backendError}");
|
$"CpuEngine native-only failed: {backendError}");
|
||||||
Log.Error($"Native backend FAILED: {backendError}");
|
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
|
||||||
return FailEarly(
|
return FailEarly(
|
||||||
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||||
CpuExitReason.NativeBackendUnavailable);
|
CpuExitReason.NativeBackendUnavailable);
|
||||||
|
|||||||
@@ -99,12 +99,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
|
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
|
||||||
_lastReportedRawSentinelRecoveries = num2;
|
_lastReportedRawSentinelRecoveries = num2;
|
||||||
}
|
}
|
||||||
// Leaf imports take a scalar-only fast path that reads its own operands and
|
|
||||||
// intentionally bypasses the SysV variadic XMM marshalling below. This is safe
|
|
||||||
// only while the leaf set contains no float-variadic or float-returning function.
|
|
||||||
// The constraint and the audited list live on IsLeafImport — do not add a
|
|
||||||
// function that consumes xmm0-7 args or returns in xmm0 to the leaf set;
|
|
||||||
// such functions must stay on the full gateway path below.
|
|
||||||
if (IsLeafImport(importStubEntry.Nid) &&
|
if (IsLeafImport(importStubEntry.Nid) &&
|
||||||
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
|
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
|
||||||
{
|
{
|
||||||
@@ -124,17 +118,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
|
cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
|
||||||
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
|
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
|
||||||
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
|
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
|
||||||
// The trampoline spills the SysV variadic XMM save area (xmm0..xmm7) into the
|
cpuContext.SetXmmRegister(
|
||||||
// 0x80 bytes immediately below the GP argpack, so variadic float args (printf
|
0,
|
||||||
// %f, and powf/logf inputs) reach the handler. xmm{i} is at argPackPtr-0x80+i*16.
|
*(ulong*)(argPackPtr - 16),
|
||||||
for (var xmmIndex = 0; xmmIndex < 8; xmmIndex++)
|
*(ulong*)(argPackPtr - 8));
|
||||||
{
|
|
||||||
var xmmSlot = argPackPtr - 0x80 + (xmmIndex * 16);
|
|
||||||
cpuContext.SetXmmRegister(
|
|
||||||
xmmIndex,
|
|
||||||
*(ulong*)xmmSlot,
|
|
||||||
*(ulong*)(xmmSlot + 8));
|
|
||||||
}
|
|
||||||
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
||||||
ulong value = cpuContext[CpuRegister.Rdi];
|
ulong value = cpuContext[CpuRegister.Rdi];
|
||||||
ulong value2 = cpuContext[CpuRegister.Rsi];
|
ulong value2 = cpuContext[CpuRegister.Rsi];
|
||||||
@@ -504,13 +491,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.Flush();
|
Console.Error.Flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Publish the handler's XMM0 back into the argpack's xmm0 save slot; the
|
|
||||||
// trampoline epilogue reloads it into the guest's XMM0, delivering float/double
|
|
||||||
// return values (powf/logf/wcstod). Harmless for int/pointer returns (XMM is
|
|
||||||
// volatile across a SysV call, so the guest never relies on a preserved XMM0).
|
|
||||||
cpuContext.GetXmmRegister(0, out var returnXmm0Low, out var returnXmm0High);
|
|
||||||
*(ulong*)(argPackPtr - 0x80) = returnXmm0Low;
|
|
||||||
*(ulong*)(argPackPtr - 0x80 + 8) = returnXmm0High;
|
|
||||||
return cpuContext[CpuRegister.Rax];
|
return cpuContext[CpuRegister.Rax];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -642,13 +622,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subset of the leaf set that additionally skips the import-call-frame
|
|
||||||
// bookkeeping. The same scalar-only (no XMM args, no XMM return) constraint
|
|
||||||
// as IsLeafImport applies — see the note there before adding entries.
|
|
||||||
// NOTE: this filter is only consulted after IsLeafImport accepts the NID;
|
|
||||||
// entries listed here but not in IsLeafImport (scePadRead, scePadOpen,
|
|
||||||
// sceUserServiceGetEvent, sceUserServiceGetPlatformPrivacySetting,
|
|
||||||
// pthread_mutex_trylock) currently take the full gateway path.
|
|
||||||
private static bool IsNoBlockLeafImport(string nid) =>
|
private static bool IsNoBlockLeafImport(string nid) =>
|
||||||
nid is
|
nid is
|
||||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||||
@@ -675,7 +648,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"xk0AcarP3V4" or // scePadOpen
|
"xk0AcarP3V4" or // scePadOpen
|
||||||
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
||||||
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
|
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
|
||||||
"K-jXhbt2gn4"; // pthread_mutex_trylock (scePthreadMutexTrylock is upoVrzMHFeE)
|
"K-jXhbt2gn4"; // scePthreadMutexTrylock
|
||||||
|
|
||||||
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
||||||
{
|
{
|
||||||
@@ -742,41 +715,22 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"1G3lF1Gg1k8" or // sceKernelOpen
|
"1G3lF1Gg1k8" or // sceKernelOpen
|
||||||
"gEpBkcwxUjw"; // sceKernelAprResolveFilepathsToIdsAndFileSizes
|
"gEpBkcwxUjw"; // sceKernelAprResolveFilepathsToIdsAndFileSizes
|
||||||
|
|
||||||
// LEAF-IMPORT REGISTRATION — scalar-only constraint.
|
|
||||||
//
|
|
||||||
// TryDispatchLeafImport is a fast path that never copies the trampoline's XMM
|
|
||||||
// save area into CpuContext and never publishes an XMM0 return back to the
|
|
||||||
// guest (see DispatchImport). Every NID listed here must therefore satisfy BOTH:
|
|
||||||
// 1. no float/double parameters (nothing passed in xmm0..xmm7), and
|
|
||||||
// 2. no float/double return value (nothing returned in xmm0).
|
|
||||||
// Integer/pointer arguments and returns only. va_list-based functions
|
|
||||||
// (vsnprintf) are safe: SysV va_list floats are read from the caller-built
|
|
||||||
// reg_save_area in guest memory, not from the callee's incoming XMM registers.
|
|
||||||
//
|
|
||||||
// If a function that consumes XMM arguments or returns in xmm0 (e.g. powf,
|
|
||||||
// logf, wcstod, sceVideoOutColorSettingsSetGamma_) is ever added here, its
|
|
||||||
// float arguments will silently arrive stale and its return value will be
|
|
||||||
// dropped. Such functions must stay on the full gateway path — do not list them.
|
|
||||||
//
|
|
||||||
// Audited 2026-07-11 (PR #59): every NID below maps to a registered
|
|
||||||
// SysAbiExport whose handler neither reads nor writes CpuContext XMM state
|
|
||||||
// and whose known guest signature is integer/pointer only.
|
|
||||||
private bool IsLeafImport(string nid)
|
private bool IsLeafImport(string nid)
|
||||||
{
|
{
|
||||||
if (nid == "1jfXLRVzisc") // sceKernelUsleep
|
if (nid == "1jfXLRVzisc")
|
||||||
{
|
{
|
||||||
return !_logUsleep;
|
return !_logUsleep;
|
||||||
}
|
}
|
||||||
|
|
||||||
return nid is
|
return nid is
|
||||||
"9UK1vLZQft4" or // scePthreadMutexLock
|
"9UK1vLZQft4" or
|
||||||
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
"tn3VlD0hG60" or
|
||||||
"7H0iTOciTLo" or // pthread_mutex_lock
|
"7H0iTOciTLo" or
|
||||||
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
"2Z+PpY6CaJg" or
|
||||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
"8aI7R7WaOlc" or
|
||||||
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
||||||
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
||||||
"qj7QZpgr9Uw" or // sceAgcUnknownQj7QZpgr9Uw (unknown NID; observed scalar: command-buffer pointer in, packet address out)
|
"qj7QZpgr9Uw" or // Gen5 graphics type-2 packet
|
||||||
"LtTouSCZjHM" or // sceAgcCbNop
|
"LtTouSCZjHM" or // sceAgcCbNop
|
||||||
"k3GhuSNmBLU" or // sceAgcCbDispatch
|
"k3GhuSNmBLU" or // sceAgcCbDispatch
|
||||||
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
|
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
|
||||||
@@ -799,24 +753,24 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
|
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
|
||||||
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
|
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
|
||||||
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
|
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
|
||||||
"a8uLzYY--tM" or // sceAmprAprCommandBufferConstructor
|
"a8uLzYY--tM" or
|
||||||
"Qs1xtplKo0U" or // sceAmprAprCommandBufferDestructor
|
"Qs1xtplKo0U" or
|
||||||
"GuchCTefuZw" or // sceAmprCommandBufferDestructor
|
"GuchCTefuZw" or
|
||||||
"N-FSPA4S3nI" or // sceAmprCommandBufferSetBuffer
|
"N-FSPA4S3nI" or
|
||||||
"baQO9ez2gL4" or // sceAmprCommandBufferReset
|
"baQO9ez2gL4" or
|
||||||
"ULvXMDz56po" or // sceAmprCommandBufferClearBuffer
|
"ULvXMDz56po" or
|
||||||
"mQ16-QdKv7k" or // sceAmprAprCommandBufferReadFile
|
"mQ16-QdKv7k" or
|
||||||
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
|
"vWU-odnS+fU" or
|
||||||
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
|
"sSAUCCU1dv4" or
|
||||||
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
|
"C+IEj+BsAFM" or
|
||||||
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
|
"tZDDEo2tE5k" or
|
||||||
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
|
"GnxKOHEawhk" or
|
||||||
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
|
"H896Pt-yB4I" or
|
||||||
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
|
"sJXyWHjP-F8" or
|
||||||
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
"ASoW5WE-UPo" or
|
||||||
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
"rqwFKI4PAiM" or
|
||||||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
"eE4Szl8sil8" or
|
||||||
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
"qvMUCyyaCSI" or
|
||||||
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
|
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
|
||||||
"TywrFKCoLGY" or // sceSaveDataInitialize3
|
"TywrFKCoLGY" or // sceSaveDataInitialize3
|
||||||
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
|
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
|
||||||
|
|||||||
@@ -1568,7 +1568,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private unsafe nint CreateImportHandlerTrampoline(int importIndex)
|
private unsafe nint CreateImportHandlerTrampoline(int importIndex)
|
||||||
{
|
{
|
||||||
void* ptr = VirtualAlloc(null, 256u, 12288u, 64u);
|
void* ptr = VirtualAlloc(null, 192u, 12288u, 64u);
|
||||||
if (ptr == null)
|
if (ptr == null)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1596,21 +1596,20 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
ptr2[num++] = 82;
|
ptr2[num++] = 82;
|
||||||
ptr2[num++] = 86;
|
ptr2[num++] = 86;
|
||||||
ptr2[num++] = 87;
|
ptr2[num++] = 87;
|
||||||
// sub rsp, 0x80 — reserve 8*16 bytes for the SysV variadic XMM save area
|
ptr2[num++] = 72;
|
||||||
ptr2[num++] = 0x48; ptr2[num++] = 0x81; ptr2[num++] = 0xEC;
|
ptr2[num++] = 131;
|
||||||
ptr2[num++] = 0x80; ptr2[num++] = 0x00; ptr2[num++] = 0x00; ptr2[num++] = 0x00;
|
ptr2[num++] = 236;
|
||||||
// movdqu [rsp + i*0x10], xmm{i} for i = 0..7 (F3 0F 7F /r, SIB=0x24 base=rsp, disp8)
|
ptr2[num++] = 16;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x44; ptr2[num++] = 0x24; ptr2[num++] = 0x00; // xmm0
|
ptr2[num++] = 243;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x4C; ptr2[num++] = 0x24; ptr2[num++] = 0x10; // xmm1
|
ptr2[num++] = 15;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x54; ptr2[num++] = 0x24; ptr2[num++] = 0x20; // xmm2
|
ptr2[num++] = 127;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x5C; ptr2[num++] = 0x24; ptr2[num++] = 0x30; // xmm3
|
ptr2[num++] = 4;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x64; ptr2[num++] = 0x24; ptr2[num++] = 0x40; // xmm4
|
ptr2[num++] = 36;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x6C; ptr2[num++] = 0x24; ptr2[num++] = 0x50; // xmm5
|
ptr2[num++] = 76;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x74; ptr2[num++] = 0x24; ptr2[num++] = 0x60; // xmm6
|
ptr2[num++] = 141;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x7C; ptr2[num++] = 0x24; ptr2[num++] = 0x70; // xmm7
|
ptr2[num++] = 100;
|
||||||
// lea r12, [rsp + 0x80] — r12 = argpack base (the 12 pushed GP regs), past the XMM area
|
ptr2[num++] = 36;
|
||||||
ptr2[num++] = 0x4C; ptr2[num++] = 0x8D; ptr2[num++] = 0xA4; ptr2[num++] = 0x24;
|
ptr2[num++] = 16;
|
||||||
ptr2[num++] = 0x80; ptr2[num++] = 0x00; ptr2[num++] = 0x00; ptr2[num++] = 0x00;
|
|
||||||
ptr2[num++] = 72;
|
ptr2[num++] = 72;
|
||||||
ptr2[num++] = 131;
|
ptr2[num++] = 131;
|
||||||
ptr2[num++] = 236;
|
ptr2[num++] = 236;
|
||||||
@@ -1658,11 +1657,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
ptr2[num++] = 131;
|
ptr2[num++] = 131;
|
||||||
ptr2[num++] = 196;
|
ptr2[num++] = 196;
|
||||||
ptr2[num++] = 40;
|
ptr2[num++] = 40;
|
||||||
// movdqu xmm0, [r12 - 0x80] — reload the return XMM0 the gateway wrote into the
|
|
||||||
// argpack's xmm0 save slot (float/double returns: powf/logf/wcstod). SysV/Win64
|
|
||||||
// XMM regs are volatile across calls, so an unconditional reload is ABI-safe.
|
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x41; ptr2[num++] = 0x0F; ptr2[num++] = 0x6F;
|
|
||||||
ptr2[num++] = 0x84; ptr2[num++] = 0x24; ptr2[num++] = 0x80; ptr2[num++] = 0xFF; ptr2[num++] = 0xFF; ptr2[num++] = 0xFF;
|
|
||||||
ptr2[num++] = 76;
|
ptr2[num++] = 76;
|
||||||
ptr2[num++] = 137;
|
ptr2[num++] = 137;
|
||||||
ptr2[num++] = 228;
|
ptr2[num++] = 228;
|
||||||
@@ -1686,12 +1680,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
ptr2[num++] = 95;
|
ptr2[num++] = 95;
|
||||||
ptr2[num++] = 195;
|
ptr2[num++] = 195;
|
||||||
uint num2 = default(uint);
|
uint num2 = default(uint);
|
||||||
if (!VirtualProtect(ptr, 256u, 32u, &num2))
|
if (!VirtualProtect(ptr, 192u, 32u, &num2))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for import dispatch stub at 0x{(nint)ptr:X16}");
|
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for import dispatch stub at 0x{(nint)ptr:X16}");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, 256u);
|
FlushInstructionCache(GetCurrentProcess(), ptr, 192u);
|
||||||
return (nint)ptr;
|
return (nint)ptr;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -4,14 +4,11 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Logging;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Memory;
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
|
|
||||||
|
|
||||||
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
||||||
private readonly object _guestAllocationGate = new();
|
private readonly object _guestAllocationGate = new();
|
||||||
private readonly object _allocationSearchHintGate = new();
|
private readonly object _allocationSearchHintGate = new();
|
||||||
@@ -1055,7 +1052,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug(message);
|
Console.Error.WriteLine($"[VMEM] {message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -11,19 +11,15 @@ using SharpEmu.Libs.VideoOut;
|
|||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Libs.AppContent;
|
using SharpEmu.Libs.AppContent;
|
||||||
using SharpEmu.Libs.SaveData;
|
using SharpEmu.Libs.SaveData;
|
||||||
using SharpEmu.Logging;
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Runtime;
|
namespace SharpEmu.Core.Runtime;
|
||||||
|
|
||||||
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
|
|
||||||
|
|
||||||
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
|
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
|
||||||
|
|
||||||
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -132,7 +128,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
||||||
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
||||||
Log.Info($"Loading: {ebootPath}");
|
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
||||||
LastExecutionDiagnostics = null;
|
LastExecutionDiagnostics = null;
|
||||||
LastExecutionTrace = null;
|
LastExecutionTrace = null;
|
||||||
LastSessionSummary = null;
|
LastSessionSummary = null;
|
||||||
@@ -142,10 +138,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
var image = LoadImage(normalizedEbootPath);
|
var image = LoadImage(normalizedEbootPath);
|
||||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||||
LogAppBundleInfo(normalizedEbootPath, image);
|
|
||||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||||
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
|
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
||||||
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
|
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
|
||||||
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
||||||
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
||||||
@@ -168,7 +163,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
processImageName);
|
processImageName);
|
||||||
if (initializerResult is { } failedInitializerResult)
|
if (initializerResult is { } failedInitializerResult)
|
||||||
{
|
{
|
||||||
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
|
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
|
||||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||||
@@ -176,8 +171,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return failedInitializerResult;
|
return failedInitializerResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info($"Dispatching, gen: {generation}");
|
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
|
||||||
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchEntry(
|
var result = _cpuDispatcher.DispatchEntry(
|
||||||
image.EntryPoint,
|
image.EntryPoint,
|
||||||
@@ -187,8 +182,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
processImageName,
|
processImageName,
|
||||||
_cpuExecutionOptions);
|
_cpuExecutionOptions);
|
||||||
|
|
||||||
Log.Info($"DispatchEntry returned: {result}");
|
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
||||||
Log.Info($"Dispatch result: {result}");
|
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
||||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||||
@@ -359,66 +354,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void LogAppBundleInfo(string ebootPath, SelfImage image)
|
|
||||||
{
|
|
||||||
var executableName = Path.GetFileName(ebootPath);
|
|
||||||
if (string.IsNullOrWhiteSpace(executableName))
|
|
||||||
{
|
|
||||||
executableName = "eboot.bin";
|
|
||||||
}
|
|
||||||
|
|
||||||
var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim();
|
|
||||||
var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim();
|
|
||||||
var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim();
|
|
||||||
var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)";
|
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
builder.AppendLine("App bundle info:");
|
|
||||||
builder.AppendLine($"- Display name: {displayName}");
|
|
||||||
builder.AppendLine($"- Version: {version}");
|
|
||||||
builder.AppendLine($"- Title ID: {titleId}");
|
|
||||||
builder.AppendLine($"- Content ID: {contentId}");
|
|
||||||
builder.AppendLine($"- Executable: {executableName}");
|
|
||||||
builder.Append("- Platform: PlayStation 5");
|
|
||||||
Log.Info(builder.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? ResolveContentId(string? bundleRoot)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(bundleRoot))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var candidate in new[]
|
|
||||||
{
|
|
||||||
Path.Combine(bundleRoot, "sce_sys", "param.json"),
|
|
||||||
Path.Combine(bundleRoot, "param.json"),
|
|
||||||
})
|
|
||||||
{
|
|
||||||
if (!File.Exists(candidate))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate));
|
|
||||||
if (doc.RootElement.TryGetProperty("contentId", out var contentId))
|
|
||||||
{
|
|
||||||
return contentId.GetString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
||||||
{
|
{
|
||||||
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
||||||
@@ -499,8 +434,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
moduleName = $"module#{i}";
|
moduleName = $"module#{i}";
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchModuleInitializer(
|
var result = _cpuDispatcher.DispatchModuleInitializer(
|
||||||
initEntryPoint,
|
initEntryPoint,
|
||||||
@@ -511,8 +446,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
_cpuExecutionOptions);
|
_cpuExecutionOptions);
|
||||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
Log.Error(
|
Console.Error.WriteLine(
|
||||||
$"Module start failed: {moduleName} -> {result}");
|
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,8 +468,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||||
|
|
||||||
var result = RunInitializerList(
|
var result = RunInitializerList(
|
||||||
$"{label}:preinit",
|
$"{label}:preinit",
|
||||||
@@ -573,8 +508,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchEntry(
|
var result = _cpuDispatcher.DispatchEntry(
|
||||||
initializerAddress,
|
initializerAddress,
|
||||||
@@ -642,7 +577,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
if (skippedModules.Length > 0)
|
if (skippedModules.Length > 0)
|
||||||
{
|
{
|
||||||
Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modulePaths.Length == 0)
|
if (modulePaths.Length == 0)
|
||||||
@@ -650,8 +585,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return loadedImages;
|
return loadedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
|
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||||
Log.Info($"Loading {modulePaths.Length} module(s)...");
|
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
|
||||||
var loadedModules = 0;
|
var loadedModules = 0;
|
||||||
var failedModules = 0;
|
var failedModules = 0;
|
||||||
var mergedImportCount = 0;
|
var mergedImportCount = 0;
|
||||||
@@ -686,18 +621,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
||||||
loadedModules++;
|
loadedModules++;
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
failedModules++;
|
failedModules++;
|
||||||
Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
|
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||||
return loadedImages;
|
return loadedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,8 +652,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
if (rebound != 0 || unresolved != 0)
|
if (rebound != 0 || unresolved != 0)
|
||||||
{
|
{
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,8 +685,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
unresolved++;
|
unresolved++;
|
||||||
@@ -763,8 +698,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Log.Error(
|
Console.Error.WriteLine(
|
||||||
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
unresolved++;
|
unresolved++;
|
||||||
@@ -773,8 +708,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
rebound++;
|
rebound++;
|
||||||
@@ -810,8 +745,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -920,8 +855,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
image.EntryPoint,
|
image.EntryPoint,
|
||||||
isMain,
|
isMain,
|
||||||
isSystemModule);
|
isSystemModule);
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
||||||
|
|||||||
@@ -11,12 +11,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<Color x:Key="SystemAccentColor">#7C5CFC</Color>
|
<Color x:Key="SystemAccentColor">#7C5CFC</Color>
|
||||||
|
|
||||||
<LinearGradientBrush x:Key="BgBrush" StartPoint="0%,0%" EndPoint="100%,100%">
|
<SolidColorBrush x:Key="BgBrush" Color="#0D1017" />
|
||||||
<GradientStop Offset="0" Color="#12151F" />
|
|
||||||
<GradientStop Offset="0.55" Color="#0D1017" />
|
|
||||||
<GradientStop Offset="1" Color="#0B0D14" />
|
|
||||||
</LinearGradientBrush>
|
|
||||||
|
|
||||||
<SolidColorBrush x:Key="ChromeBrush" Color="#090C12" />
|
<SolidColorBrush x:Key="ChromeBrush" Color="#090C12" />
|
||||||
<SolidColorBrush x:Key="CardBrush" Color="#141924" />
|
<SolidColorBrush x:Key="CardBrush" Color="#141924" />
|
||||||
<SolidColorBrush x:Key="CardBorderBrush" Color="#232B3A" />
|
<SolidColorBrush x:Key="CardBorderBrush" Color="#232B3A" />
|
||||||
@@ -29,8 +24,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<SolidColorBrush x:Key="DangerBrush" Color="#E5484D" />
|
<SolidColorBrush x:Key="DangerBrush" Color="#E5484D" />
|
||||||
<SolidColorBrush x:Key="DangerHoverBrush" Color="#F2555A" />
|
<SolidColorBrush x:Key="DangerHoverBrush" Color="#F2555A" />
|
||||||
<SolidColorBrush x:Key="SuccessBrush" Color="#46C46B" />
|
<SolidColorBrush x:Key="SuccessBrush" Color="#46C46B" />
|
||||||
<SolidColorBrush x:Key="TileHoverBrush" Color="#1A2130" />
|
|
||||||
<SolidColorBrush x:Key="TileSelectedBrush" Color="#212A3F" />
|
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
|
|
||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
@@ -45,7 +38,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="12" />
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
<Setter Property="Padding" Value="16" />
|
<Setter Property="Padding" Value="16" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
@@ -68,10 +61,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Setter Property="Margin" Value="0,0,0,6" />
|
<Setter Property="Margin" Value="0,0,0,6" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="TextBox">
|
|
||||||
<Setter Property="CornerRadius" Value="8" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="Button.accent">
|
<Style Selector="Button.accent">
|
||||||
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
|
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
|
||||||
<Setter Property="Foreground" Value="White" />
|
<Setter Property="Foreground" Value="White" />
|
||||||
@@ -109,41 +98,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="ToggleButton.ghost">
|
|
||||||
<Setter Property="Background" Value="Transparent" />
|
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
|
||||||
<Setter Property="Padding" Value="12,7" />
|
|
||||||
<Setter Property="CornerRadius" Value="8" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ToggleButton.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ToggleButton.ghost:checked /template/ ContentPresenter#PART_ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="ContextMenu">
|
|
||||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
|
||||||
<Setter Property="CornerRadius" Value="10" />
|
|
||||||
<Setter Property="Padding" Value="6" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ContextMenu MenuItem">
|
|
||||||
<Setter Property="Padding" Value="10,7" />
|
|
||||||
<Setter Property="CornerRadius" Value="7" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ContextMenu Separator">
|
|
||||||
<Setter Property="Background" Value="{StaticResource CardBorderBrush}" />
|
|
||||||
<Setter Property="Height" Value="1" />
|
|
||||||
<Setter Property="Margin" Value="8,4" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="ListBox.console">
|
<Style Selector="ListBox.console">
|
||||||
<Setter Property="Background" Value="#0B0E14" />
|
<Setter Property="Background" Value="#0B0E14" />
|
||||||
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas, Courier New, monospace" />
|
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas, Courier New, monospace" />
|
||||||
@@ -154,44 +108,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Setter Property="MinHeight" Value="0" />
|
<Setter Property="MinHeight" Value="0" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- Cover-art library grid -->
|
<Style Selector="ListBox.library ListBoxItem">
|
||||||
<Style Selector="ListBox.tileGrid ListBoxItem">
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
<Setter Property="Padding" Value="10" />
|
<Setter Property="Margin" Value="4,2" />
|
||||||
<Setter Property="Margin" Value="5" />
|
<Setter Property="Padding" Value="8,4" />
|
||||||
<Setter Property="CornerRadius" Value="14" />
|
|
||||||
<Setter Property="Background" Value="Transparent" />
|
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
|
||||||
<Setter Property="BorderBrush" Value="Transparent" />
|
|
||||||
<Setter Property="RenderTransform" Value="translateY(0px)" />
|
|
||||||
<Setter Property="Transitions">
|
|
||||||
<Transitions>
|
|
||||||
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.12" />
|
|
||||||
</Transitions>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover">
|
|
||||||
<Setter Property="RenderTransform" Value="translateY(-3px)" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{StaticResource TileHoverBrush}" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBox.tileGrid ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
|
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBox.tileGrid ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
|
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource AccentHoverBrush}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="Border.coverShadow">
|
|
||||||
<Setter Property="CornerRadius" Value="10" />
|
|
||||||
<Setter Property="BoxShadow" Value="0 6 14 0 #55000000" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Border.coverClip">
|
|
||||||
<Setter Property="CornerRadius" Value="10" />
|
|
||||||
<Setter Property="ClipToBounds" Value="True" />
|
|
||||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
|
||||||
</Style>
|
</Style>
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
|
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using System.IO;
|
|
||||||
using LibAtrac9.Utilities;
|
|
||||||
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Stores the configuration data needed to decode or encode an ATRAC9 stream.
|
|
||||||
/// </summary>
|
|
||||||
public class Atrac9Config
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The 4-byte ATRAC9 configuration data.
|
|
||||||
/// </summary>
|
|
||||||
public byte[] ConfigData { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A 4-bit value specifying one of 16 sample rates.
|
|
||||||
/// </summary>
|
|
||||||
public int SampleRateIndex { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// A 3-bit value specifying one of 6 substream channel mappings.
|
|
||||||
/// </summary>
|
|
||||||
public int ChannelConfigIndex { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// An 11-bit value containing the average size of a single frame.
|
|
||||||
/// </summary>
|
|
||||||
public int FrameBytes { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// A 2-bit value indicating how many frames are in each superframe.
|
|
||||||
/// </summary>
|
|
||||||
public int SuperframeIndex { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The channel mapping used by the ATRAC9 stream.
|
|
||||||
/// </summary>
|
|
||||||
public ChannelConfig ChannelConfig { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The total number of channels in the ATRAC9 stream.
|
|
||||||
/// </summary>
|
|
||||||
public int ChannelCount { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The sample rate of the ATRAC9 stream.
|
|
||||||
/// </summary>
|
|
||||||
public int SampleRate { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates whether the ATRAC9 stream has a <see cref="SampleRateIndex"/> of 8 or above.
|
|
||||||
/// </summary>
|
|
||||||
public bool HighSampleRate { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The number of frames in each superframe.
|
|
||||||
/// </summary>
|
|
||||||
public int FramesPerSuperframe { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The number of samples in one frame as an exponent of 2.
|
|
||||||
/// <see cref="FrameSamples"/> = 2^<see cref="FrameSamplesPower"/>.
|
|
||||||
/// </summary>
|
|
||||||
public int FrameSamplesPower { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The number of samples in one frame.
|
|
||||||
/// </summary>
|
|
||||||
public int FrameSamples { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The number of bytes in one superframe.
|
|
||||||
/// </summary>
|
|
||||||
public int SuperframeBytes { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The number of samples in one superframe.
|
|
||||||
/// </summary>
|
|
||||||
public int SuperframeSamples { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads ATRAC9 configuration data and calculates the stream parameters from it.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="configData">The processed ATRAC9 configuration.</param>
|
|
||||||
public Atrac9Config(byte[] configData)
|
|
||||||
{
|
|
||||||
if (configData == null || configData.Length != 4)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Config data must be 4 bytes long");
|
|
||||||
}
|
|
||||||
|
|
||||||
ReadConfigData(configData, out int a, out int b, out int c, out int d);
|
|
||||||
SampleRateIndex = a;
|
|
||||||
ChannelConfigIndex = b;
|
|
||||||
FrameBytes = c;
|
|
||||||
SuperframeIndex = d;
|
|
||||||
ConfigData = configData;
|
|
||||||
|
|
||||||
FramesPerSuperframe = 1 << SuperframeIndex;
|
|
||||||
SuperframeBytes = FrameBytes << SuperframeIndex;
|
|
||||||
ChannelConfig = Tables.ChannelConfig[ChannelConfigIndex];
|
|
||||||
|
|
||||||
ChannelCount = ChannelConfig.ChannelCount;
|
|
||||||
SampleRate = Tables.SampleRates[SampleRateIndex];
|
|
||||||
HighSampleRate = SampleRateIndex > 7;
|
|
||||||
FrameSamplesPower = Tables.SamplingRateIndexToFrameSamplesPower[SampleRateIndex];
|
|
||||||
FrameSamples = 1 << FrameSamplesPower;
|
|
||||||
SuperframeSamples = FrameSamples * FramesPerSuperframe;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadConfigData(byte[] configData, out int sampleRateIndex, out int channelConfigIndex, out int frameBytes, out int superframeIndex)
|
|
||||||
{
|
|
||||||
var reader = new BitReader(configData);
|
|
||||||
|
|
||||||
int header = reader.ReadInt(8);
|
|
||||||
sampleRateIndex = reader.ReadInt(4);
|
|
||||||
channelConfigIndex = reader.ReadInt(3);
|
|
||||||
int validationBit = reader.ReadInt(1);
|
|
||||||
frameBytes = reader.ReadInt(11) + 1;
|
|
||||||
superframeIndex = reader.ReadInt(2);
|
|
||||||
|
|
||||||
if (header != 0xFE || validationBit != 0)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("ATRAC9 Config Data is invalid");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using System;
|
|
||||||
using LibAtrac9.Utilities;
|
|
||||||
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Decodes an ATRAC9 stream into 16-bit PCM.
|
|
||||||
/// </summary>
|
|
||||||
public class Atrac9Decoder
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The config data for the current ATRAC9 stream.
|
|
||||||
/// </summary>
|
|
||||||
public Atrac9Config Config { get; private set; }
|
|
||||||
|
|
||||||
private Frame Frame { get; set; }
|
|
||||||
private BitReader Reader { get; set; }
|
|
||||||
private bool _initialized;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets up the decoder to decode an ATRAC9 stream based on the information in <paramref name="configData"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="configData">A 4-byte value containing information about the ATRAC9 stream.</param>
|
|
||||||
public void Initialize(byte[] configData)
|
|
||||||
{
|
|
||||||
Config = new Atrac9Config(configData);
|
|
||||||
Frame = new Frame(Config);
|
|
||||||
Reader = new BitReader(null);
|
|
||||||
_initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decodes one superframe of ATRAC9 data.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="atrac9Data">The ATRAC9 data to decode. The array must be at least
|
|
||||||
/// <see cref="Config"/>.<see cref="Atrac9Config.SuperframeBytes"/> bytes long.</param>
|
|
||||||
/// <param name="pcmOut">A buffer that the decoded PCM data will be placed in.
|
|
||||||
/// The array must have dimensions of at least [<see cref="Config"/>.<see cref="Atrac9Config.ChannelCount"/>]
|
|
||||||
/// [<see cref="Config"/>.<see cref="Atrac9Config.SuperframeSamples"/>].</param>
|
|
||||||
public void Decode(byte[] atrac9Data, short[][] pcmOut)
|
|
||||||
{
|
|
||||||
if (!_initialized) throw new InvalidOperationException("Decoder must be initialized before decoding.");
|
|
||||||
|
|
||||||
ValidateDecodeBuffers(atrac9Data, pcmOut);
|
|
||||||
Reader.SetBuffer(atrac9Data);
|
|
||||||
DecodeSuperFrame(pcmOut);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ValidateDecodeBuffers(byte[] atrac9Buffer, short[][] pcmBuffer)
|
|
||||||
{
|
|
||||||
if (atrac9Buffer == null) throw new ArgumentNullException(nameof(atrac9Buffer));
|
|
||||||
if (pcmBuffer == null) throw new ArgumentNullException(nameof(pcmBuffer));
|
|
||||||
|
|
||||||
if (atrac9Buffer.Length < Config.SuperframeBytes)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("ATRAC9 buffer is too small");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pcmBuffer.Length < Config.ChannelCount)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("PCM buffer is too small");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < Config.ChannelCount; i++)
|
|
||||||
{
|
|
||||||
if (pcmBuffer[i]?.Length < Config.SuperframeSamples)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("PCM buffer is too small");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DecodeSuperFrame(short[][] pcmOut)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < Config.FramesPerSuperframe; i++)
|
|
||||||
{
|
|
||||||
Frame.FrameIndex = i;
|
|
||||||
DecodeFrame(Reader, Frame);
|
|
||||||
PcmFloatToShort(pcmOut, i * Config.FrameSamples);
|
|
||||||
Reader.AlignPosition(8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PcmFloatToShort(short[][] pcmOut, int start)
|
|
||||||
{
|
|
||||||
int endSample = start + Config.FrameSamples;
|
|
||||||
int channelNum = 0;
|
|
||||||
foreach (Block block in Frame.Blocks)
|
|
||||||
{
|
|
||||||
foreach (Channel channel in block.Channels)
|
|
||||||
{
|
|
||||||
double[] pcmSrc = channel.Pcm;
|
|
||||||
short[] pcmDest = pcmOut[channelNum++];
|
|
||||||
for (int d = 0, s = start; s < endSample; d++, s++)
|
|
||||||
{
|
|
||||||
double sample = pcmSrc[d];
|
|
||||||
// Not using Math.Round because it's ~20x slower on 64-bit
|
|
||||||
int roundedSample = (int)Math.Floor(sample + 0.5);
|
|
||||||
pcmDest[s] = Helpers.Clamp16(roundedSample);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DecodeFrame(BitReader reader, Frame frame)
|
|
||||||
{
|
|
||||||
Unpack.UnpackFrame(reader, frame);
|
|
||||||
|
|
||||||
foreach (Block block in frame.Blocks)
|
|
||||||
{
|
|
||||||
Quantization.DequantizeSpectra(block);
|
|
||||||
Stereo.ApplyIntensityStereo(block);
|
|
||||||
Quantization.ScaleSpectrum(block);
|
|
||||||
BandExtension.ApplyBandExtension(block);
|
|
||||||
ImdctBlock(block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ImdctBlock(Block block)
|
|
||||||
{
|
|
||||||
foreach (Channel channel in block.Channels)
|
|
||||||
{
|
|
||||||
channel.Mdct.RunImdct(channel.Spectra, channel.Pcm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// An Xorshift RNG used by the ATRAC9 codec
|
|
||||||
/// </summary>
|
|
||||||
internal class Atrac9Rng
|
|
||||||
{
|
|
||||||
private ushort _stateA;
|
|
||||||
private ushort _stateB;
|
|
||||||
private ushort _stateC;
|
|
||||||
private ushort _stateD;
|
|
||||||
|
|
||||||
public Atrac9Rng(ushort seed)
|
|
||||||
{
|
|
||||||
int startValue = 0x4D93 * (seed ^ (seed >> 14));
|
|
||||||
|
|
||||||
_stateA = (ushort)(3 - startValue);
|
|
||||||
_stateB = (ushort)(2 - startValue);
|
|
||||||
_stateC = (ushort)(1 - startValue);
|
|
||||||
_stateD = (ushort)(0 - startValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ushort Next()
|
|
||||||
{
|
|
||||||
ushort t = (ushort)(_stateD ^ (_stateD << 5));
|
|
||||||
_stateD = _stateC;
|
|
||||||
_stateC = _stateB;
|
|
||||||
_stateB = _stateA;
|
|
||||||
_stateA = (ushort)(t ^ _stateA ^ ((t ^ (_stateA >> 5)) >> 4));
|
|
||||||
return _stateA;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
internal static class BandExtension
|
|
||||||
{
|
|
||||||
public static void ApplyBandExtension(Block block)
|
|
||||||
{
|
|
||||||
if (!block.BandExtensionEnabled || !block.HasExtensionData) return;
|
|
||||||
|
|
||||||
foreach (Channel channel in block.Channels)
|
|
||||||
{
|
|
||||||
ApplyBandExtensionChannel(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ApplyBandExtensionChannel(Channel channel)
|
|
||||||
{
|
|
||||||
int groupAUnit = channel.Block.QuantizationUnitCount;
|
|
||||||
int[] scaleFactors = channel.ScaleFactors;
|
|
||||||
double[] spectra = channel.Spectra;
|
|
||||||
double[] scales = channel.BexScales;
|
|
||||||
int[] values = channel.BexValues;
|
|
||||||
|
|
||||||
GetBexBandInfo(out int bandCount, out int groupBUnit, out int groupCUnit, groupAUnit);
|
|
||||||
int totalUnits = Math.Max(groupCUnit, 22);
|
|
||||||
|
|
||||||
int groupABin = Tables.QuantUnitToCoeffIndex[groupAUnit];
|
|
||||||
int groupBBin = Tables.QuantUnitToCoeffIndex[groupBUnit];
|
|
||||||
int groupCBin = Tables.QuantUnitToCoeffIndex[groupCUnit];
|
|
||||||
int totalBins = Tables.QuantUnitToCoeffIndex[totalUnits];
|
|
||||||
|
|
||||||
FillHighFrequencies(spectra, groupABin, groupBBin, groupCBin, totalBins);
|
|
||||||
|
|
||||||
switch (channel.BexMode)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
int bexQuantUnits = totalUnits - groupAUnit;
|
|
||||||
|
|
||||||
switch (bandCount)
|
|
||||||
{
|
|
||||||
case 3:
|
|
||||||
scales[0] = BexMode0Bands3[0][values[0]];
|
|
||||||
scales[1] = BexMode0Bands3[1][values[0]];
|
|
||||||
scales[2] = BexMode0Bands3[2][values[1]];
|
|
||||||
scales[3] = BexMode0Bands3[3][values[2]];
|
|
||||||
scales[4] = BexMode0Bands3[4][values[3]];
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
scales[0] = BexMode0Bands4[0][values[0]];
|
|
||||||
scales[1] = BexMode0Bands4[1][values[0]];
|
|
||||||
scales[2] = BexMode0Bands4[2][values[1]];
|
|
||||||
scales[3] = BexMode0Bands4[3][values[2]];
|
|
||||||
scales[4] = BexMode0Bands4[4][values[3]];
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
scales[0] = BexMode0Bands5[0][values[0]];
|
|
||||||
scales[1] = BexMode0Bands5[1][values[1]];
|
|
||||||
scales[2] = BexMode0Bands5[2][values[1]];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
scales[bexQuantUnits - 1] = Tables.SpectrumScale[scaleFactors[groupAUnit]];
|
|
||||||
|
|
||||||
AddNoiseToSpectrum(channel, Tables.QuantUnitToCoeffIndex[totalUnits - 1],
|
|
||||||
Tables.QuantUnitToCoeffCount[totalUnits - 1]);
|
|
||||||
ScaleBexQuantUnits(spectra, scales, groupAUnit, totalUnits);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
for (int i = groupAUnit; i < totalUnits; i++)
|
|
||||||
{
|
|
||||||
scales[i - groupAUnit] = Tables.SpectrumScale[scaleFactors[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
AddNoiseToSpectrum(channel, groupABin, totalBins - groupABin);
|
|
||||||
ScaleBexQuantUnits(spectra, scales, groupAUnit, totalUnits);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
double groupAScale2 = BexMode2Scale[values[0]];
|
|
||||||
double groupBScale2 = BexMode2Scale[values[1]];
|
|
||||||
|
|
||||||
for (int i = groupABin; i < groupBBin; i++)
|
|
||||||
{
|
|
||||||
spectra[i] *= groupAScale2;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = groupBBin; i < groupCBin; i++)
|
|
||||||
{
|
|
||||||
spectra[i] *= groupBScale2;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case 3:
|
|
||||||
double rate = Math.Pow(2, BexMode3Rate[values[1]]);
|
|
||||||
double scale = BexMode3Initial[values[0]];
|
|
||||||
for (int i = groupABin; i < totalBins; i++)
|
|
||||||
{
|
|
||||||
scale *= rate;
|
|
||||||
spectra[i] *= scale;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case 4:
|
|
||||||
double mult = BexMode4Multiplier[values[0]];
|
|
||||||
double groupAScale4 = 0.7079468 * mult;
|
|
||||||
double groupBScale4 = 0.5011902 * mult;
|
|
||||||
double groupCScale4 = 0.3548279 * mult;
|
|
||||||
|
|
||||||
for (int i = groupABin; i < groupBBin; i++)
|
|
||||||
{
|
|
||||||
spectra[i] *= groupAScale4;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = groupBBin; i < groupCBin; i++)
|
|
||||||
{
|
|
||||||
spectra[i] *= groupBScale4;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = groupCBin; i < totalBins; i++)
|
|
||||||
{
|
|
||||||
spectra[i] *= groupCScale4;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ScaleBexQuantUnits(double[] spectra, double[] scales, int startUnit, int totalUnits)
|
|
||||||
{
|
|
||||||
for (int i = startUnit; i < totalUnits; i++)
|
|
||||||
{
|
|
||||||
for (int k = Tables.QuantUnitToCoeffIndex[i]; k < Tables.QuantUnitToCoeffIndex[i + 1]; k++)
|
|
||||||
{
|
|
||||||
spectra[k] *= scales[i - startUnit];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void FillHighFrequencies(double[] spectra, int groupABin, int groupBBin, int groupCBin, int totalBins)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < groupBBin - groupABin; i++)
|
|
||||||
{
|
|
||||||
spectra[groupABin + i] = spectra[groupABin - i - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < groupCBin - groupBBin; i++)
|
|
||||||
{
|
|
||||||
spectra[groupBBin + i] = spectra[groupBBin - i - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < totalBins - groupCBin; i++)
|
|
||||||
{
|
|
||||||
spectra[groupCBin + i] = spectra[groupCBin - i - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddNoiseToSpectrum(Channel channel, int index, int count)
|
|
||||||
{
|
|
||||||
if (channel.Rng == null)
|
|
||||||
{
|
|
||||||
int[] sf = channel.ScaleFactors;
|
|
||||||
ushort seed = (ushort)(543 * (sf[8] + sf[12] + sf[15] + 1));
|
|
||||||
channel.Rng = new Atrac9Rng(seed);
|
|
||||||
}
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
channel.Spectra[i + index] = channel.Rng.Next() / 65535.0 * 2.0 - 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void GetBexBandInfo(out int bandCount, out int groupAUnit, out int groupBUnit, int quantUnits)
|
|
||||||
{
|
|
||||||
groupAUnit = BexGroupInfo[quantUnits - 13][0];
|
|
||||||
groupBUnit = BexGroupInfo[quantUnits - 13][1];
|
|
||||||
bandCount = BexGroupInfo[quantUnits - 13][2];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly byte[][] BexGroupInfo =
|
|
||||||
{
|
|
||||||
new byte[] {16, 21, 0},
|
|
||||||
new byte[] {18, 22, 1},
|
|
||||||
new byte[] {20, 22, 2},
|
|
||||||
new byte[] {21, 22, 3},
|
|
||||||
new byte[] {21, 22, 3},
|
|
||||||
new byte[] {23, 24, 4},
|
|
||||||
new byte[] {23, 24, 4},
|
|
||||||
new byte[] {24, 24, 5}
|
|
||||||
};
|
|
||||||
|
|
||||||
// [mode][bands]
|
|
||||||
|
|
||||||
public static readonly byte[][] BexEncodedValueCounts =
|
|
||||||
{
|
|
||||||
new byte[] {0, 0, 0, 4, 4, 2},
|
|
||||||
new byte[] {0, 0, 0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 2, 2, 1},
|
|
||||||
new byte[] {0, 0, 0, 2, 2, 2},
|
|
||||||
new byte[] {1, 1, 1, 0, 0, 0}
|
|
||||||
};
|
|
||||||
|
|
||||||
// [mode][bands][valueIndex]
|
|
||||||
|
|
||||||
public static readonly byte[][][] BexDataLengths =
|
|
||||||
{
|
|
||||||
new[] {
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {5, 4, 3, 3},
|
|
||||||
new byte[] {4, 4, 3, 4},
|
|
||||||
new byte[] {4, 5, 0, 0}
|
|
||||||
}, new[] {
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0}
|
|
||||||
}, new[] {
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {6, 6, 0, 0},
|
|
||||||
new byte[] {6, 6, 0, 0},
|
|
||||||
new byte[] {6, 0, 0, 0}
|
|
||||||
}, new[] {
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {4, 4, 0, 0},
|
|
||||||
new byte[] {4, 4, 0, 0},
|
|
||||||
new byte[] {4, 4, 0, 0}
|
|
||||||
}, new[] {
|
|
||||||
new byte[] {3, 0, 0, 0},
|
|
||||||
new byte[] {3, 0, 0, 0},
|
|
||||||
new byte[] {3, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0},
|
|
||||||
new byte[] {0, 0, 0, 0}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly double[][] BexMode0Bands3 =
|
|
||||||
{
|
|
||||||
new[] {
|
|
||||||
0.000000e+0, 1.988220e-1, 2.514343e-1, 2.960510e-1,
|
|
||||||
3.263550e-1, 3.771362e-1, 3.786926e-1, 4.540405e-1,
|
|
||||||
4.877625e-1, 5.262451e-1, 5.447083e-1, 5.737000e-1,
|
|
||||||
6.212158e-1, 6.222839e-1, 6.560974e-1, 6.896667e-1,
|
|
||||||
7.555542e-1, 7.677917e-1, 7.918091e-1, 7.971497e-1,
|
|
||||||
8.188171e-1, 8.446045e-1, 9.790649e-1, 9.822083e-1,
|
|
||||||
9.846191e-1, 9.859314e-1, 9.863586e-1, 9.863892e-1,
|
|
||||||
9.873352e-1, 9.881287e-1, 9.898682e-1, 9.913330e-1
|
|
||||||
}, new[] {
|
|
||||||
0.000000e+0, 9.982910e-1, 7.592773e-2, 7.179565e-1,
|
|
||||||
9.851379e-1, 5.340271e-1, 9.013672e-1, 6.349182e-1,
|
|
||||||
7.226257e-1, 1.948547e-1, 7.628174e-1, 9.873657e-1,
|
|
||||||
8.112183e-1, 2.715454e-1, 9.734192e-1, 1.443787e-1,
|
|
||||||
4.640198e-1, 3.249207e-1, 3.790894e-1, 8.276367e-2,
|
|
||||||
5.954590e-1, 2.864380e-1, 9.806824e-1, 7.929077e-1,
|
|
||||||
6.292114e-1, 4.887085e-1, 2.905273e-1, 1.301880e-1,
|
|
||||||
3.140869e-1, 5.482483e-1, 4.210815e-1, 1.182861e-1
|
|
||||||
}, new[] {
|
|
||||||
0.000000e+0, 3.155518e-2, 8.581543e-2, 1.364746e-1,
|
|
||||||
1.858826e-1, 2.368469e-1, 2.888184e-1, 3.432617e-1,
|
|
||||||
4.012451e-1, 4.623108e-1, 5.271301e-1, 5.954895e-1,
|
|
||||||
6.681213e-1, 7.448425e-1, 8.245239e-1, 9.097290e-1
|
|
||||||
}, new[] {
|
|
||||||
0.000000e+0, 4.418945e-2, 1.303711e-1, 2.273560e-1,
|
|
||||||
3.395996e-1, 4.735718e-1, 6.267090e-1, 8.003845e-1
|
|
||||||
}, new[] {
|
|
||||||
0.000000e+0, 2.804565e-2, 9.683228e-2, 1.849976e-1,
|
|
||||||
3.005981e-1, 4.470520e-1, 6.168518e-1, 8.007813e-1
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly double[][] BexMode0Bands4 =
|
|
||||||
{
|
|
||||||
new[] {
|
|
||||||
0.000000e+0, 2.708740e-1, 3.479614e-1, 3.578186e-1,
|
|
||||||
5.083618e-1, 5.299072e-1, 5.819092e-1, 6.381836e-1,
|
|
||||||
7.276917e-1, 7.595520e-1, 7.878723e-1, 9.707336e-1,
|
|
||||||
9.713135e-1, 9.736023e-1, 9.759827e-1, 9.832458e-1
|
|
||||||
}, new[] {
|
|
||||||
0.000000e+0, 2.330627e-1, 5.891418e-1, 7.170410e-1,
|
|
||||||
2.036438e-1, 1.613464e-1, 6.668701e-1, 9.481201e-1,
|
|
||||||
9.769897e-1, 5.111694e-1, 3.522644e-1, 8.209534e-1,
|
|
||||||
2.933960e-1, 9.757690e-1, 5.289917e-1, 4.372253e-1
|
|
||||||
}, new[] {
|
|
||||||
0.000000e+0, 4.360962e-2, 1.056519e-1, 1.590576e-1,
|
|
||||||
2.078857e-1, 2.572937e-1, 3.082581e-1, 3.616028e-1,
|
|
||||||
4.191589e-1, 4.792175e-1, 5.438538e-1, 6.125183e-1,
|
|
||||||
6.841125e-1, 7.589417e-1, 8.365173e-1, 9.148254e-1
|
|
||||||
}, new[] {
|
|
||||||
0.000000e+0, 4.074097e-2, 1.164551e-1, 2.077026e-1,
|
|
||||||
3.184509e-1, 4.532166e-1, 6.124268e-1, 7.932129e-1
|
|
||||||
}, new[] {
|
|
||||||
0.000000e+0, 8.880615e-3, 2.932739e-2, 5.593872e-2,
|
|
||||||
8.825684e-2, 1.259155e-1, 1.721497e-1, 2.270813e-1,
|
|
||||||
2.901611e-1, 3.579712e-1, 4.334106e-1, 5.147095e-1,
|
|
||||||
6.023254e-1, 6.956177e-1, 7.952881e-1, 8.977356e-1
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly double[][] BexMode0Bands5 =
|
|
||||||
{
|
|
||||||
new[] {
|
|
||||||
0.000000e+0, 7.379150e-2, 1.806335e-1, 2.687073e-1,
|
|
||||||
3.407898e-1, 4.047546e-1, 4.621887e-1, 5.168762e-1,
|
|
||||||
5.703125e-1, 6.237488e-1, 6.763611e-1, 7.288208e-1,
|
|
||||||
7.808533e-1, 8.337708e-1, 8.874512e-1, 9.418030e-1
|
|
||||||
}, new[] {
|
|
||||||
0.000000e+0, 7.980347e-2, 1.615295e-1, 1.665649e-1,
|
|
||||||
1.822205e-1, 2.185669e-1, 2.292175e-1, 2.456665e-1,
|
|
||||||
2.666321e-1, 3.306580e-1, 3.330688e-1, 3.765259e-1,
|
|
||||||
4.085083e-1, 4.400024e-1, 4.407654e-1, 4.817505e-1,
|
|
||||||
4.924011e-1, 5.320740e-1, 5.893860e-1, 6.131287e-1,
|
|
||||||
6.212463e-1, 6.278076e-1, 6.308899e-1, 7.660828e-1,
|
|
||||||
7.850647e-1, 7.910461e-1, 7.929382e-1, 8.038330e-1,
|
|
||||||
9.834900e-1, 9.846191e-1, 9.852295e-1, 9.862671e-1
|
|
||||||
}, new[] {
|
|
||||||
0.000000e+0, 6.084290e-1, 3.672791e-1, 3.151855e-1,
|
|
||||||
1.488953e-1, 2.571716e-1, 5.103455e-1, 3.311157e-1,
|
|
||||||
5.426025e-2, 4.254456e-1, 7.998352e-1, 7.873230e-1,
|
|
||||||
5.418701e-1, 2.925110e-1, 8.468628e-2, 1.410522e-1,
|
|
||||||
9.819641e-1, 9.609070e-1, 3.530884e-2, 9.729004e-2,
|
|
||||||
5.758362e-1, 9.941711e-1, 7.215576e-1, 7.183228e-1,
|
|
||||||
2.028809e-1, 9.588623e-2, 2.032166e-1, 1.338806e-1,
|
|
||||||
5.003357e-1, 1.874390e-1, 9.804993e-1, 1.107788e-1
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly double[] BexMode2Scale =
|
|
||||||
{
|
|
||||||
4.272461e-4, 1.312256e-3, 2.441406e-3, 3.692627e-3,
|
|
||||||
4.913330e-3, 6.134033e-3, 7.507324e-3, 8.972168e-3,
|
|
||||||
1.049805e-2, 1.223755e-2, 1.406860e-2, 1.599121e-2,
|
|
||||||
1.800537e-2, 2.026367e-2, 2.264404e-2, 2.517700e-2,
|
|
||||||
2.792358e-2, 3.073120e-2, 3.344727e-2, 3.631592e-2,
|
|
||||||
3.952026e-2, 4.275513e-2, 4.608154e-2, 4.968262e-2,
|
|
||||||
5.355835e-2, 5.783081e-2, 6.195068e-2, 6.677246e-2,
|
|
||||||
7.196045e-2, 7.745361e-2, 8.319092e-2, 8.993530e-2,
|
|
||||||
9.759521e-2, 1.056213e-1, 1.138916e-1, 1.236267e-1,
|
|
||||||
1.348267e-1, 1.470337e-1, 1.603394e-1, 1.755676e-1,
|
|
||||||
1.905823e-1, 2.071228e-1, 2.245178e-1, 2.444153e-1,
|
|
||||||
2.658997e-1, 2.897644e-1, 3.146057e-1, 3.450012e-1,
|
|
||||||
3.766174e-1, 4.122620e-1, 4.505615e-1, 4.893799e-1,
|
|
||||||
5.305481e-1, 5.731201e-1, 6.157837e-1, 6.580811e-1,
|
|
||||||
6.985168e-1, 7.435303e-1, 7.865906e-1, 8.302612e-1,
|
|
||||||
8.718567e-1, 9.125671e-1, 9.575806e-1, 9.996643e-1
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly double[] BexMode3Initial =
|
|
||||||
{
|
|
||||||
3.491211e-1, 5.371094e-1, 6.782227e-1, 7.910156e-1,
|
|
||||||
9.057617e-1, 1.024902e+0, 1.156250e+0, 1.290527e+0,
|
|
||||||
1.458984e+0, 1.664551e+0, 1.929688e+0, 2.278320e+0,
|
|
||||||
2.831543e+0, 3.659180e+0, 5.257813e+0, 8.373047e+0
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly double[] BexMode3Rate =
|
|
||||||
{
|
|
||||||
-2.913818e-1, -2.541504e-1, -1.664429e-1, -1.476440e-1,
|
|
||||||
-1.342163e-1, -1.220703e-1, -1.117554e-1, -1.026611e-1,
|
|
||||||
-9.436035e-2, -8.483887e-2, -7.476807e-2, -6.304932e-2,
|
|
||||||
-4.492188e-2, -2.447510e-2, +1.831055e-4, +4.174805e-2
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly double[] BexMode4Multiplier =
|
|
||||||
{
|
|
||||||
3.610229e-2, 1.260681e-1, 2.227478e-1, 3.338318e-1,
|
|
||||||
4.662170e-1, 6.221313e-1, 7.989197e-1, 9.939575e-1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
internal static class BitAllocation
|
|
||||||
{
|
|
||||||
public static void CreateGradient(Block block)
|
|
||||||
{
|
|
||||||
int valueCount = block.GradientEndValue - block.GradientStartValue;
|
|
||||||
int unitCount = block.GradientEndUnit - block.GradientStartUnit;
|
|
||||||
|
|
||||||
for (int i = 0; i < block.GradientEndUnit; i++)
|
|
||||||
{
|
|
||||||
block.Gradient[i] = block.GradientStartValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = block.GradientEndUnit; i <= block.QuantizationUnitCount; i++)
|
|
||||||
{
|
|
||||||
block.Gradient[i] = block.GradientEndValue;
|
|
||||||
}
|
|
||||||
if (unitCount <= 0) return;
|
|
||||||
if (valueCount == 0) return;
|
|
||||||
|
|
||||||
byte[] curve = Tables.GradientCurves[unitCount - 1];
|
|
||||||
if (valueCount <= 0)
|
|
||||||
{
|
|
||||||
double scale = (-valueCount - 1) / 31.0;
|
|
||||||
int baseVal = block.GradientStartValue - 1;
|
|
||||||
for (int i = block.GradientStartUnit; i < block.GradientEndUnit; i++)
|
|
||||||
{
|
|
||||||
block.Gradient[i] = baseVal - (int)(curve[i - block.GradientStartUnit] * scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
double scale = (valueCount - 1) / 31.0;
|
|
||||||
int baseVal = block.GradientStartValue + 1;
|
|
||||||
for (int i = block.GradientStartUnit; i < block.GradientEndUnit; i++)
|
|
||||||
{
|
|
||||||
block.Gradient[i] = baseVal + (int)(curve[i - block.GradientStartUnit] * scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void CalculateMask(Channel channel)
|
|
||||||
{
|
|
||||||
Array.Clear(channel.PrecisionMask, 0, channel.PrecisionMask.Length);
|
|
||||||
for (int i = 1; i < channel.Block.QuantizationUnitCount; i++)
|
|
||||||
{
|
|
||||||
int delta = channel.ScaleFactors[i] - channel.ScaleFactors[i - 1];
|
|
||||||
if (delta > 1)
|
|
||||||
{
|
|
||||||
channel.PrecisionMask[i] += Math.Min(delta - 1, 5);
|
|
||||||
}
|
|
||||||
else if (delta < -1)
|
|
||||||
{
|
|
||||||
channel.PrecisionMask[i - 1] += Math.Min(delta * -1 - 1, 5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void CalculatePrecisions(Channel channel)
|
|
||||||
{
|
|
||||||
Block block = channel.Block;
|
|
||||||
|
|
||||||
if (block.GradientMode != 0)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
|
||||||
{
|
|
||||||
channel.Precisions[i] = channel.ScaleFactors[i] + channel.PrecisionMask[i] - block.Gradient[i];
|
|
||||||
if (channel.Precisions[i] > 0)
|
|
||||||
{
|
|
||||||
switch (block.GradientMode)
|
|
||||||
{
|
|
||||||
case 1:
|
|
||||||
channel.Precisions[i] /= 2;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
channel.Precisions[i] = 3 * channel.Precisions[i] / 8;
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
channel.Precisions[i] /= 4;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
|
||||||
{
|
|
||||||
channel.Precisions[i] = channel.ScaleFactors[i] - block.Gradient[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
|
||||||
{
|
|
||||||
if (channel.Precisions[i] < 1)
|
|
||||||
{
|
|
||||||
channel.Precisions[i] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < block.GradientBoundary; i++)
|
|
||||||
{
|
|
||||||
channel.Precisions[i]++;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
|
||||||
{
|
|
||||||
channel.PrecisionsFine[i] = 0;
|
|
||||||
if (channel.Precisions[i] > 15)
|
|
||||||
{
|
|
||||||
channel.PrecisionsFine[i] = channel.Precisions[i] - 15;
|
|
||||||
channel.Precisions[i] = 15;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[][] GenerateGradientCurves()
|
|
||||||
{
|
|
||||||
byte[] main =
|
|
||||||
{
|
|
||||||
01, 01, 01, 01, 02, 02, 02, 02, 03, 03, 03, 04, 04, 05, 05, 06, 07, 08, 09, 10, 11, 12, 13, 15,
|
|
||||||
16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 26, 27, 27, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30
|
|
||||||
};
|
|
||||||
var curves = new byte[main.Length][];
|
|
||||||
|
|
||||||
for (int length = 1; length <= main.Length; length++)
|
|
||||||
{
|
|
||||||
curves[length - 1] = new byte[length];
|
|
||||||
for (int i = 0; i < length; i++)
|
|
||||||
{
|
|
||||||
curves[length - 1][i] = main[i * main.Length / length];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return curves;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
internal class Block
|
|
||||||
{
|
|
||||||
public Atrac9Config Config { get; }
|
|
||||||
public BlockType BlockType { get; }
|
|
||||||
public int BlockIndex { get; }
|
|
||||||
public Frame Frame { get; }
|
|
||||||
|
|
||||||
public Channel[] Channels { get; }
|
|
||||||
public int ChannelCount { get; }
|
|
||||||
|
|
||||||
public bool FirstInSuperframe { get; set; }
|
|
||||||
public bool ReuseBandParams { get; set; }
|
|
||||||
|
|
||||||
public int BandCount { get; set; }
|
|
||||||
public int StereoBand { get; set; }
|
|
||||||
public int ExtensionBand { get; set; }
|
|
||||||
public int QuantizationUnitCount { get; set; }
|
|
||||||
public int StereoQuantizationUnit { get; set; }
|
|
||||||
public int ExtensionUnit { get; set; }
|
|
||||||
public int QuantizationUnitsPrev { get; set; }
|
|
||||||
|
|
||||||
public int[] Gradient { get; } = new int[31];
|
|
||||||
public int GradientMode { get; set; }
|
|
||||||
public int GradientStartUnit { get; set; }
|
|
||||||
public int GradientStartValue { get; set; }
|
|
||||||
public int GradientEndUnit { get; set; }
|
|
||||||
public int GradientEndValue { get; set; }
|
|
||||||
public int GradientBoundary { get; set; }
|
|
||||||
|
|
||||||
public int PrimaryChannelIndex { get; set; }
|
|
||||||
public int[] JointStereoSigns { get; } = new int[30];
|
|
||||||
public bool HasJointStereoSigns { get; set; }
|
|
||||||
public Channel PrimaryChannel => Channels[PrimaryChannelIndex == 0 ? 0 : 1];
|
|
||||||
public Channel SecondaryChannel => Channels[PrimaryChannelIndex == 0 ? 1 : 0];
|
|
||||||
|
|
||||||
public bool BandExtensionEnabled { get; set; }
|
|
||||||
public bool HasExtensionData { get; set; }
|
|
||||||
public int BexDataLength { get; set; }
|
|
||||||
public int BexMode { get; set; }
|
|
||||||
|
|
||||||
public Block(Frame parentFrame, int blockIndex)
|
|
||||||
{
|
|
||||||
Frame = parentFrame;
|
|
||||||
BlockIndex = blockIndex;
|
|
||||||
Config = parentFrame.Config;
|
|
||||||
BlockType = Config.ChannelConfig.BlockTypes[blockIndex];
|
|
||||||
ChannelCount = BlockTypeToChannelCount(BlockType);
|
|
||||||
Channels = new Channel[ChannelCount];
|
|
||||||
for (int i = 0; i < ChannelCount; i++)
|
|
||||||
{
|
|
||||||
Channels[i] = new Channel(this, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int BlockTypeToChannelCount(BlockType blockType)
|
|
||||||
{
|
|
||||||
switch (blockType)
|
|
||||||
{
|
|
||||||
case BlockType.Mono:
|
|
||||||
return 1;
|
|
||||||
case BlockType.Stereo:
|
|
||||||
return 2;
|
|
||||||
case BlockType.LFE:
|
|
||||||
return 1;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An ATRAC9 block (substream) type
|
|
||||||
/// </summary>
|
|
||||||
public enum BlockType
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Mono ATRAC9 block
|
|
||||||
/// </summary>
|
|
||||||
Mono = 0,
|
|
||||||
/// <summary>
|
|
||||||
/// Stereo ATRAC9 block
|
|
||||||
/// </summary>
|
|
||||||
Stereo = 1,
|
|
||||||
/// <summary>
|
|
||||||
/// Low-frequency effects ATRAC9 block
|
|
||||||
/// </summary>
|
|
||||||
LFE = 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using LibAtrac9.Utilities;
|
|
||||||
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
internal class Channel
|
|
||||||
{
|
|
||||||
public Atrac9Config Config { get; }
|
|
||||||
public int ChannelIndex { get; }
|
|
||||||
public bool IsPrimary => Block.PrimaryChannelIndex == ChannelIndex;
|
|
||||||
public Block Block { get; }
|
|
||||||
public Mdct Mdct { get; }
|
|
||||||
|
|
||||||
public double[] Pcm { get; } = new double[256];
|
|
||||||
public double[] Spectra { get; } = new double[256];
|
|
||||||
|
|
||||||
public int CodedQuantUnits { get; set; }
|
|
||||||
public int ScaleFactorCodingMode { get; set; }
|
|
||||||
public int[] ScaleFactors { get; } = new int[31];
|
|
||||||
public int[] ScaleFactorsPrev { get; } = new int[31];
|
|
||||||
|
|
||||||
public int[] Precisions { get; } = new int[30];
|
|
||||||
public int[] PrecisionsFine { get; } = new int[30];
|
|
||||||
public int[] PrecisionMask { get; } = new int[30];
|
|
||||||
|
|
||||||
public int[] SpectraValuesBuffer { get; } = new int[16];
|
|
||||||
public int[] CodebookSet { get; } = new int[30];
|
|
||||||
|
|
||||||
public int[] QuantizedSpectra { get; } = new int[256];
|
|
||||||
public int[] QuantizedSpectraFine { get; } = new int[256];
|
|
||||||
|
|
||||||
public int BexMode { get; set; }
|
|
||||||
public int BexValueCount { get; set; }
|
|
||||||
public int[] BexValues { get; } = new int[4];
|
|
||||||
public double[] BexScales { get; } = new double[6];
|
|
||||||
public Atrac9Rng Rng { get; set; }
|
|
||||||
|
|
||||||
public Channel(Block parentBlock, int channelIndex)
|
|
||||||
{
|
|
||||||
Block = parentBlock;
|
|
||||||
ChannelIndex = channelIndex;
|
|
||||||
Config = parentBlock.Config;
|
|
||||||
Mdct = new Mdct(Config.FrameSamplesPower, Tables.ImdctWindow[Config.FrameSamplesPower - 6]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateCodedUnits() =>
|
|
||||||
CodedQuantUnits = IsPrimary ? Block.QuantizationUnitCount : Block.StereoQuantizationUnit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Describes the channel mapping for an ATRAC9 stream
|
|
||||||
/// </summary>
|
|
||||||
public class ChannelConfig
|
|
||||||
{
|
|
||||||
internal ChannelConfig(params BlockType[] blockTypes)
|
|
||||||
{
|
|
||||||
BlockCount = blockTypes.Length;
|
|
||||||
BlockTypes = blockTypes;
|
|
||||||
foreach (BlockType type in blockTypes)
|
|
||||||
{
|
|
||||||
ChannelCount += Block.BlockTypeToChannelCount(type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The number of blocks or substreams in the ATRAC9 stream
|
|
||||||
/// </summary>
|
|
||||||
public int BlockCount { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The type of each block or substream in the ATRAC9 stream
|
|
||||||
/// </summary>
|
|
||||||
public BlockType[] BlockTypes { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The number of channels in the ATRAC9 stream
|
|
||||||
/// </summary>
|
|
||||||
public int ChannelCount { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
internal class Frame
|
|
||||||
{
|
|
||||||
public Atrac9Config Config { get; }
|
|
||||||
public int FrameIndex { get; set; }
|
|
||||||
public Block[] Blocks { get; }
|
|
||||||
|
|
||||||
public Frame(Atrac9Config config)
|
|
||||||
{
|
|
||||||
Config = config;
|
|
||||||
Blocks = new Block[config.ChannelConfig.BlockCount];
|
|
||||||
|
|
||||||
for (int i = 0; i < config.ChannelConfig.BlockCount; i++)
|
|
||||||
{
|
|
||||||
Blocks[i] = new Block(this, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using System;
|
|
||||||
using LibAtrac9.Utilities;
|
|
||||||
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
internal class HuffmanCodebook
|
|
||||||
{
|
|
||||||
public HuffmanCodebook(short[] codes, byte[] bits, byte valueCountPower)
|
|
||||||
{
|
|
||||||
Codes = codes;
|
|
||||||
Bits = bits;
|
|
||||||
if (Codes == null || Bits == null) return;
|
|
||||||
|
|
||||||
ValueCount = 1 << valueCountPower;
|
|
||||||
ValueCountPower = valueCountPower;
|
|
||||||
ValueBits = Helpers.Log2(codes.Length) >> valueCountPower;
|
|
||||||
ValueMax = 1 << ValueBits;
|
|
||||||
|
|
||||||
int max = 0;
|
|
||||||
foreach (byte bitSize in bits)
|
|
||||||
{
|
|
||||||
max = Math.Max(max, bitSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
MaxBitSize = max;
|
|
||||||
Lookup = CreateLookupTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] CreateLookupTable()
|
|
||||||
{
|
|
||||||
if (Codes == null || Bits == null) return null;
|
|
||||||
|
|
||||||
int tableSize = 1 << MaxBitSize;
|
|
||||||
var dest = new byte[tableSize];
|
|
||||||
|
|
||||||
for (int i = 0; i < Bits.Length; i++)
|
|
||||||
{
|
|
||||||
if (Bits[i] == 0) continue;
|
|
||||||
int unusedBits = MaxBitSize - Bits[i];
|
|
||||||
|
|
||||||
int start = Codes[i] << unusedBits;
|
|
||||||
int length = 1 << unusedBits;
|
|
||||||
int end = start + length;
|
|
||||||
|
|
||||||
for (int j = start; j < end; j++)
|
|
||||||
{
|
|
||||||
dest[j] = (byte)i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dest;
|
|
||||||
}
|
|
||||||
|
|
||||||
public short[] Codes { get; }
|
|
||||||
public byte[] Bits { get; }
|
|
||||||
public byte[] Lookup { get; }
|
|
||||||
public int ValueCount { get; }
|
|
||||||
public int ValueCountPower { get; }
|
|
||||||
public int ValueBits { get; }
|
|
||||||
public int ValueMax { get; }
|
|
||||||
public int MaxBitSize { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2018 Alex Barney
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
internal static class Quantization
|
|
||||||
{
|
|
||||||
public static void DequantizeSpectra(Block block)
|
|
||||||
{
|
|
||||||
foreach (Channel channel in block.Channels)
|
|
||||||
{
|
|
||||||
Array.Clear(channel.Spectra, 0, channel.Spectra.Length);
|
|
||||||
|
|
||||||
for (int i = 0; i < channel.CodedQuantUnits; i++)
|
|
||||||
{
|
|
||||||
DequantizeQuantUnit(channel, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DequantizeQuantUnit(Channel channel, int band)
|
|
||||||
{
|
|
||||||
int subBandIndex = Tables.QuantUnitToCoeffIndex[band];
|
|
||||||
int subBandCount = Tables.QuantUnitToCoeffCount[band];
|
|
||||||
double stepSize = Tables.QuantizerStepSize[channel.Precisions[band]];
|
|
||||||
double stepSizeFine = Tables.QuantizerFineStepSize[channel.PrecisionsFine[band]];
|
|
||||||
|
|
||||||
for (int sb = 0; sb < subBandCount; sb++)
|
|
||||||
{
|
|
||||||
double coarse = channel.QuantizedSpectra[subBandIndex + sb] * stepSize;
|
|
||||||
double fine = channel.QuantizedSpectraFine[subBandIndex + sb] * stepSizeFine;
|
|
||||||
channel.Spectra[subBandIndex + sb] = coarse + fine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ScaleSpectrum(Block block)
|
|
||||||
{
|
|
||||||
foreach (Channel channel in block.Channels)
|
|
||||||
{
|
|
||||||
ScaleSpectrum(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ScaleSpectrum(Channel channel)
|
|
||||||
{
|
|
||||||
int quantUnitCount = channel.Block.QuantizationUnitCount;
|
|
||||||
double[] spectra = channel.Spectra;
|
|
||||||
|
|
||||||
for (int i = 0; i < quantUnitCount; i++)
|
|
||||||
{
|
|
||||||
for (int sb = Tables.QuantUnitToCoeffIndex[i]; sb < Tables.QuantUnitToCoeffIndex[i + 1]; sb++)
|
|
||||||
{
|
|
||||||
spectra[sb] *= Tables.SpectrumScale[channel.ScaleFactors[i]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using LibAtrac9.Utilities;
|
|
||||||
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
internal static class ScaleFactors
|
|
||||||
{
|
|
||||||
public static void Read(BitReader reader, Channel channel)
|
|
||||||
{
|
|
||||||
Array.Clear(channel.ScaleFactors, 0, channel.ScaleFactors.Length);
|
|
||||||
|
|
||||||
channel.ScaleFactorCodingMode = reader.ReadInt(2);
|
|
||||||
if (channel.ChannelIndex == 0)
|
|
||||||
{
|
|
||||||
switch (channel.ScaleFactorCodingMode)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
ReadVlcDeltaOffset(reader, channel);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
ReadClcOffset(reader, channel);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
|
|
||||||
ReadVlcDistanceToBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
|
|
||||||
ReadVlcDeltaOffsetWithBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
switch (channel.ScaleFactorCodingMode)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
ReadVlcDeltaOffset(reader, channel);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
ReadVlcDistanceToBaseline(reader, channel, channel.Block.Channels[0].ScaleFactors, channel.Block.ExtensionUnit);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
ReadVlcDeltaOffsetWithBaseline(reader, channel, channel.Block.Channels[0].ScaleFactors, channel.Block.ExtensionUnit);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
|
|
||||||
ReadVlcDistanceToBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
|
|
||||||
{
|
|
||||||
if (channel.ScaleFactors[i] < 0 || channel.ScaleFactors[i] > 31)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Scale factor values are out of range.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Array.Copy(channel.ScaleFactors, channel.ScaleFactorsPrev, channel.ScaleFactors.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadClcOffset(BitReader reader, Channel channel)
|
|
||||||
{
|
|
||||||
const int maxBits = 5;
|
|
||||||
int[] sf = channel.ScaleFactors;
|
|
||||||
int bitLength = reader.ReadInt(2) + 2;
|
|
||||||
int baseValue = bitLength < maxBits ? reader.ReadInt(maxBits) : 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
|
|
||||||
{
|
|
||||||
sf[i] = reader.ReadInt(bitLength) + baseValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadVlcDeltaOffset(BitReader reader, Channel channel)
|
|
||||||
{
|
|
||||||
int weightIndex = reader.ReadInt(3);
|
|
||||||
byte[] weights = ScaleFactorWeights[weightIndex];
|
|
||||||
|
|
||||||
int[] sf = channel.ScaleFactors;
|
|
||||||
int baseValue = reader.ReadInt(5);
|
|
||||||
int bitLength = reader.ReadInt(2) + 3;
|
|
||||||
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsUnsigned[bitLength];
|
|
||||||
|
|
||||||
sf[0] = reader.ReadInt(bitLength);
|
|
||||||
|
|
||||||
for (int i = 1; i < channel.Block.ExtensionUnit; i++)
|
|
||||||
{
|
|
||||||
int delta = Unpack.ReadHuffmanValue(codebook, reader);
|
|
||||||
sf[i] = (sf[i - 1] + delta) & (codebook.ValueMax - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
|
|
||||||
{
|
|
||||||
sf[i] += baseValue - weights[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadVlcDistanceToBaseline(BitReader reader, Channel channel, int[] baseline, int baselineLength)
|
|
||||||
{
|
|
||||||
int[] sf = channel.ScaleFactors;
|
|
||||||
int bitLength = reader.ReadInt(2) + 2;
|
|
||||||
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsSigned[bitLength];
|
|
||||||
int unitCount = Math.Min(channel.Block.ExtensionUnit, baselineLength);
|
|
||||||
|
|
||||||
for (int i = 0; i < unitCount; i++)
|
|
||||||
{
|
|
||||||
int distance = Unpack.ReadHuffmanValue(codebook, reader, true);
|
|
||||||
sf[i] = (baseline[i] + distance) & 31;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = unitCount; i < channel.Block.ExtensionUnit; i++)
|
|
||||||
{
|
|
||||||
sf[i] = reader.ReadInt(5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadVlcDeltaOffsetWithBaseline(BitReader reader, Channel channel, int[] baseline, int baselineLength)
|
|
||||||
{
|
|
||||||
int[] sf = channel.ScaleFactors;
|
|
||||||
int baseValue = reader.ReadOffsetBinary(5, BitReader.OffsetBias.Negative);
|
|
||||||
int bitLength = reader.ReadInt(2) + 1;
|
|
||||||
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsUnsigned[bitLength];
|
|
||||||
int unitCount = Math.Min(channel.Block.ExtensionUnit, baselineLength);
|
|
||||||
|
|
||||||
sf[0] = reader.ReadInt(bitLength);
|
|
||||||
|
|
||||||
for (int i = 1; i < unitCount; i++)
|
|
||||||
{
|
|
||||||
int delta = Unpack.ReadHuffmanValue(codebook, reader);
|
|
||||||
sf[i] = (sf[i - 1] + delta) & (codebook.ValueMax - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < unitCount; i++)
|
|
||||||
{
|
|
||||||
sf[i] += baseValue + baseline[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = unitCount; i < channel.Block.ExtensionUnit; i++)
|
|
||||||
{
|
|
||||||
sf[i] = reader.ReadInt(5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly byte[][] ScaleFactorWeights =
|
|
||||||
{
|
|
||||||
new byte[] {
|
|
||||||
0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 3, 2, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 10, 12, 12, 12
|
|
||||||
}, new byte[] {
|
|
||||||
3, 2, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 2, 3, 3, 4, 5, 7, 10, 10, 10
|
|
||||||
}, new byte[] {
|
|
||||||
0, 2, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 8, 9, 12, 12, 12
|
|
||||||
}, new byte[] {
|
|
||||||
0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 8, 8, 10, 11, 11, 12, 13, 13, 13, 13
|
|
||||||
}, new byte[] {
|
|
||||||
0, 2, 2, 3, 3, 4, 4, 5, 4, 5, 5, 5, 5, 6, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 11, 12, 12, 13, 13, 14, 14, 14
|
|
||||||
}, new byte[] {
|
|
||||||
1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5, 6, 7, 7, 9, 11, 11, 11
|
|
||||||
}, new byte[] {
|
|
||||||
0, 5, 8, 10, 11, 11, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 12,
|
|
||||||
12, 13, 15, 15, 15
|
|
||||||
}, new byte[] {
|
|
||||||
0, 2, 3, 4, 5, 6, 6, 7, 7, 8, 8, 8, 9, 9, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13,
|
|
||||||
15, 15, 15
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
internal static class Stereo
|
|
||||||
{
|
|
||||||
public static void ApplyIntensityStereo(Block block)
|
|
||||||
{
|
|
||||||
if (block.BlockType != BlockType.Stereo) return;
|
|
||||||
|
|
||||||
int totalUnits = block.QuantizationUnitCount;
|
|
||||||
int stereoUnits = block.StereoQuantizationUnit;
|
|
||||||
if (stereoUnits >= totalUnits) return;
|
|
||||||
|
|
||||||
Channel source = block.PrimaryChannel;
|
|
||||||
Channel dest = block.SecondaryChannel;
|
|
||||||
|
|
||||||
for (int i = stereoUnits; i < totalUnits; i++)
|
|
||||||
{
|
|
||||||
int sign = block.JointStereoSigns[i];
|
|
||||||
for (int sb = Tables.QuantUnitToCoeffIndex[i]; sb < Tables.QuantUnitToCoeffIndex[i + 1]; sb++)
|
|
||||||
{
|
|
||||||
if (sign > 0)
|
|
||||||
{
|
|
||||||
dest.Spectra[sb] = -source.Spectra[sb];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dest.Spectra[sb] = source.Spectra[sb];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using System;
|
|
||||||
using static LibAtrac9.HuffmanCodebooks;
|
|
||||||
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
internal static class Tables
|
|
||||||
{
|
|
||||||
public static int MaxHuffPrecision(bool highSampleRate) => highSampleRate ? 1 : 7;
|
|
||||||
public static int MinBandCount(bool highSampleRate) => highSampleRate ? 1 : 3;
|
|
||||||
public static int MaxExtensionBand(bool highSampleRate) => highSampleRate ? 16 : 18;
|
|
||||||
|
|
||||||
public static readonly int[] SampleRates =
|
|
||||||
{
|
|
||||||
11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000,
|
|
||||||
44100, 48000, 64000, 88200, 96000, 128000, 176400, 192000
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly byte[] SamplingRateIndexToFrameSamplesPower = { 6, 6, 7, 7, 7, 8, 8, 8, 6, 6, 7, 7, 7, 8, 8, 8 };
|
|
||||||
|
|
||||||
// From sampling rate index
|
|
||||||
public static readonly byte[] MaxBandCount = { 8, 8, 12, 12, 12, 18, 18, 18, 8, 8, 12, 12, 12, 16, 16, 16 };
|
|
||||||
public static readonly byte[] BandToQuantUnitCount = { 0, 4, 8, 10, 12, 13, 14, 15, 16, 18, 20, 21, 22, 23, 24, 25, 26, 28, 30 };
|
|
||||||
|
|
||||||
public static readonly byte[] QuantUnitToCoeffCount =
|
|
||||||
{
|
|
||||||
02, 02, 02, 02, 02, 02, 02, 02, 04, 04, 04, 04, 08, 08, 08,
|
|
||||||
08, 08, 08, 08, 08, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly short[] QuantUnitToCoeffIndex =
|
|
||||||
{
|
|
||||||
0, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56,
|
|
||||||
64, 72, 80, 88, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly byte[] QuantUnitToCodebookIndex =
|
|
||||||
{
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
|
|
||||||
2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly ChannelConfig[] ChannelConfig =
|
|
||||||
{
|
|
||||||
new ChannelConfig(BlockType.Mono),
|
|
||||||
new ChannelConfig(BlockType.Mono, BlockType.Mono),
|
|
||||||
new ChannelConfig(BlockType.Stereo),
|
|
||||||
new ChannelConfig(BlockType.Stereo, BlockType.Mono, BlockType.LFE, BlockType.Stereo),
|
|
||||||
new ChannelConfig(BlockType.Stereo, BlockType.Mono, BlockType.LFE, BlockType.Stereo, BlockType.Stereo),
|
|
||||||
new ChannelConfig(BlockType.Stereo, BlockType.Stereo)
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly HuffmanCodebook[] HuffmanScaleFactorsUnsigned =
|
|
||||||
GenerateHuffmanCodebooks(HuffmanScaleFactorsACodes, HuffmanScaleFactorsABits, HuffmanScaleFactorsGroupSizes);
|
|
||||||
|
|
||||||
public static readonly HuffmanCodebook[] HuffmanScaleFactorsSigned =
|
|
||||||
GenerateHuffmanCodebooks(HuffmanScaleFactorsBCodes, HuffmanScaleFactorsBBits, HuffmanScaleFactorsGroupSizes);
|
|
||||||
|
|
||||||
public static readonly HuffmanCodebook[][][] HuffmanSpectrum =
|
|
||||||
{
|
|
||||||
GenerateHuffmanCodebooks(HuffmanSpectrumACodes, HuffmanSpectrumABits, HuffmanSpectrumAGroupSizes),
|
|
||||||
GenerateHuffmanCodebooks(HuffmanSpectrumBCodes, HuffmanSpectrumBBits, HuffmanSpectrumBGroupSizes)
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly double[][] ImdctWindow = { GenerateImdctWindow(6), GenerateImdctWindow(7), GenerateImdctWindow(8) };
|
|
||||||
|
|
||||||
public static readonly double[] SpectrumScale = Generate(32, SpectrumScaleFunction);
|
|
||||||
public static readonly double[] QuantizerStepSize = Generate(16, QuantizerStepSizeFunction);
|
|
||||||
public static readonly double[] QuantizerFineStepSize = Generate(16, QuantizerFineStepSizeFunction);
|
|
||||||
|
|
||||||
public static readonly byte[][] GradientCurves = BitAllocation.GenerateGradientCurves();
|
|
||||||
|
|
||||||
private static double QuantizerStepSizeFunction(int x) => 2.0 / ((1 << (x + 1)) - 1);
|
|
||||||
private static double QuantizerFineStepSizeFunction(int x) => QuantizerStepSizeFunction(x) / ushort.MaxValue;
|
|
||||||
private static double SpectrumScaleFunction(int x) => Math.Pow(2, x - 15);
|
|
||||||
|
|
||||||
private static double[] GenerateImdctWindow(int frameSizePower)
|
|
||||||
{
|
|
||||||
int frameSize = 1 << frameSizePower;
|
|
||||||
var output = new double[frameSize];
|
|
||||||
|
|
||||||
double[] a1 = GenerateMdctWindow(frameSizePower);
|
|
||||||
|
|
||||||
for (int i = 0; i < frameSize; i++)
|
|
||||||
{
|
|
||||||
output[i] = a1[i] / (a1[frameSize - 1 - i] * a1[frameSize - 1 - i] + a1[i] * a1[i]);
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double[] GenerateMdctWindow(int frameSizePower)
|
|
||||||
{
|
|
||||||
int frameSize = 1 << frameSizePower;
|
|
||||||
var output = new double[frameSize];
|
|
||||||
|
|
||||||
for (int i = 0; i < frameSize; i++)
|
|
||||||
{
|
|
||||||
output[i] = (Math.Sin(((i + 0.5) / frameSize - 0.5) * Math.PI) + 1.0) * 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static T[] Generate<T>(int count, Func<int, T> elementGenerator)
|
|
||||||
{
|
|
||||||
var table = new T[count];
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
table[i] = elementGenerator(i);
|
|
||||||
}
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,426 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using LibAtrac9.Utilities;
|
|
||||||
|
|
||||||
namespace LibAtrac9
|
|
||||||
{
|
|
||||||
internal static class Unpack
|
|
||||||
{
|
|
||||||
public static void UnpackFrame(BitReader reader, Frame frame)
|
|
||||||
{
|
|
||||||
foreach (Block block in frame.Blocks)
|
|
||||||
{
|
|
||||||
UnpackBlock(reader, block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UnpackBlock(BitReader reader, Block block)
|
|
||||||
{
|
|
||||||
ReadBlockHeader(reader, block);
|
|
||||||
|
|
||||||
if (block.BlockType == BlockType.LFE)
|
|
||||||
{
|
|
||||||
UnpackLfeBlock(reader, block);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UnpackStandardBlock(reader, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.AlignPosition(8);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadBlockHeader(BitReader reader, Block block)
|
|
||||||
{
|
|
||||||
bool firstInSuperframe = block.Frame.FrameIndex == 0;
|
|
||||||
block.FirstInSuperframe = !reader.ReadBool();
|
|
||||||
block.ReuseBandParams = reader.ReadBool();
|
|
||||||
|
|
||||||
if (block.FirstInSuperframe != firstInSuperframe)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstInSuperframe && block.ReuseBandParams && block.BlockType != BlockType.LFE)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UnpackStandardBlock(BitReader reader, Block block)
|
|
||||||
{
|
|
||||||
Channel[] channels = block.Channels;
|
|
||||||
|
|
||||||
if (!block.ReuseBandParams)
|
|
||||||
{
|
|
||||||
ReadBandParams(reader, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
ReadGradientParams(reader, block);
|
|
||||||
BitAllocation.CreateGradient(block);
|
|
||||||
ReadStereoParams(reader, block);
|
|
||||||
ReadExtensionParams(reader, block);
|
|
||||||
|
|
||||||
foreach (Channel channel in channels)
|
|
||||||
{
|
|
||||||
channel.UpdateCodedUnits();
|
|
||||||
|
|
||||||
ScaleFactors.Read(reader, channel);
|
|
||||||
BitAllocation.CalculateMask(channel);
|
|
||||||
BitAllocation.CalculatePrecisions(channel);
|
|
||||||
CalculateSpectrumCodebookIndex(channel);
|
|
||||||
|
|
||||||
ReadSpectra(reader, channel);
|
|
||||||
ReadSpectraFine(reader, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
block.QuantizationUnitsPrev = block.BandExtensionEnabled ? block.ExtensionUnit : block.QuantizationUnitCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadBandParams(BitReader reader, Block block)
|
|
||||||
{
|
|
||||||
int minBandCount = Tables.MinBandCount(block.Config.HighSampleRate);
|
|
||||||
int maxExtensionBand = Tables.MaxExtensionBand(block.Config.HighSampleRate);
|
|
||||||
block.BandCount = reader.ReadInt(4);
|
|
||||||
block.BandCount += minBandCount;
|
|
||||||
block.QuantizationUnitCount = Tables.BandToQuantUnitCount[block.BandCount];
|
|
||||||
if (block.BandCount < minBandCount || block.BandCount >
|
|
||||||
Tables.MaxBandCount[block.Config.SampleRateIndex])
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (block.BlockType == BlockType.Stereo)
|
|
||||||
{
|
|
||||||
block.StereoBand = reader.ReadInt(4);
|
|
||||||
block.StereoBand += minBandCount;
|
|
||||||
block.StereoQuantizationUnit = Tables.BandToQuantUnitCount[block.StereoBand];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
block.StereoBand = block.BandCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
block.BandExtensionEnabled = reader.ReadBool();
|
|
||||||
if (block.BandExtensionEnabled)
|
|
||||||
{
|
|
||||||
block.ExtensionBand = reader.ReadInt(4);
|
|
||||||
block.ExtensionBand += minBandCount;
|
|
||||||
|
|
||||||
if (block.ExtensionBand < block.BandCount || block.ExtensionBand > maxExtensionBand)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
|
|
||||||
block.ExtensionUnit = Tables.BandToQuantUnitCount[block.ExtensionBand];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
block.ExtensionBand = block.BandCount;
|
|
||||||
block.ExtensionUnit = block.QuantizationUnitCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadGradientParams(BitReader reader, Block block)
|
|
||||||
{
|
|
||||||
block.GradientMode = reader.ReadInt(2);
|
|
||||||
if (block.GradientMode > 0)
|
|
||||||
{
|
|
||||||
block.GradientEndUnit = 31;
|
|
||||||
block.GradientEndValue = 31;
|
|
||||||
block.GradientStartUnit = reader.ReadInt(5);
|
|
||||||
block.GradientStartValue = reader.ReadInt(5);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
block.GradientStartUnit = reader.ReadInt(6);
|
|
||||||
block.GradientEndUnit = reader.ReadInt(6) + 1;
|
|
||||||
block.GradientStartValue = reader.ReadInt(5);
|
|
||||||
block.GradientEndValue = reader.ReadInt(5);
|
|
||||||
}
|
|
||||||
block.GradientBoundary = reader.ReadInt(4);
|
|
||||||
|
|
||||||
if (block.GradientBoundary > block.QuantizationUnitCount)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
if (block.GradientStartUnit < 1 || block.GradientStartUnit >= 48)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
if (block.GradientEndUnit < 1 || block.GradientEndUnit >= 48)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
if (block.GradientStartUnit > block.GradientEndUnit)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
if (block.GradientStartValue < 0 || block.GradientStartValue >= 32)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
if (block.GradientEndValue < 0 || block.GradientEndValue >= 32)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadStereoParams(BitReader reader, Block block)
|
|
||||||
{
|
|
||||||
if (block.BlockType != BlockType.Stereo) return;
|
|
||||||
|
|
||||||
block.PrimaryChannelIndex = reader.ReadInt(1);
|
|
||||||
block.HasJointStereoSigns = reader.ReadBool();
|
|
||||||
if (block.HasJointStereoSigns)
|
|
||||||
{
|
|
||||||
for (int i = block.StereoQuantizationUnit; i < block.QuantizationUnitCount; i++)
|
|
||||||
{
|
|
||||||
block.JointStereoSigns[i] = reader.ReadInt(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Array.Clear(block.JointStereoSigns, 0, block.JointStereoSigns.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadExtensionParams(BitReader reader, Block block)
|
|
||||||
{
|
|
||||||
// ReSharper disable once RedundantAssignment
|
|
||||||
int bexBand = 0;
|
|
||||||
if (block.BandExtensionEnabled)
|
|
||||||
{
|
|
||||||
BandExtension.GetBexBandInfo(out bexBand, out _, out _, block.QuantizationUnitCount);
|
|
||||||
if (block.BlockType == BlockType.Stereo)
|
|
||||||
{
|
|
||||||
ReadHeader(block.Channels[1]);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
reader.Position += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
block.HasExtensionData = reader.ReadBool();
|
|
||||||
|
|
||||||
if (!block.HasExtensionData) return;
|
|
||||||
if (!block.BandExtensionEnabled)
|
|
||||||
{
|
|
||||||
block.BexMode = reader.ReadInt(2);
|
|
||||||
block.BexDataLength = reader.ReadInt(5);
|
|
||||||
reader.Position += block.BexDataLength;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReadHeader(block.Channels[0]);
|
|
||||||
|
|
||||||
block.BexDataLength = reader.ReadInt(5);
|
|
||||||
if (block.BexDataLength <= 0) return;
|
|
||||||
int bexDataEnd = reader.Position + block.BexDataLength;
|
|
||||||
|
|
||||||
ReadData(block.Channels[0]);
|
|
||||||
|
|
||||||
if (block.BlockType == BlockType.Stereo)
|
|
||||||
{
|
|
||||||
ReadData(block.Channels[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure we didn't read too many bits
|
|
||||||
if (reader.Position > bexDataEnd)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReadHeader(Channel channel)
|
|
||||||
{
|
|
||||||
int bexMode = reader.ReadInt(2);
|
|
||||||
channel.BexMode = bexBand > 2 ? bexMode : 4;
|
|
||||||
channel.BexValueCount = BandExtension.BexEncodedValueCounts[channel.BexMode][bexBand];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReadData(Channel channel)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < channel.BexValueCount; i++)
|
|
||||||
{
|
|
||||||
int dataLength = BandExtension.BexDataLengths[channel.BexMode][bexBand][i];
|
|
||||||
channel.BexValues[i] = reader.ReadInt(dataLength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void CalculateSpectrumCodebookIndex(Channel channel)
|
|
||||||
{
|
|
||||||
Array.Clear(channel.CodebookSet, 0, channel.CodebookSet.Length);
|
|
||||||
int quantUnits = channel.CodedQuantUnits;
|
|
||||||
int[] sf = channel.ScaleFactors;
|
|
||||||
|
|
||||||
if (quantUnits <= 1) return;
|
|
||||||
if (channel.Config.HighSampleRate) return;
|
|
||||||
|
|
||||||
// Temporarily setting this value allows for simpler code by
|
|
||||||
// making the last value a non-special case.
|
|
||||||
int originalScaleTmp = sf[quantUnits];
|
|
||||||
sf[quantUnits] = sf[quantUnits - 1];
|
|
||||||
|
|
||||||
int avg = 0;
|
|
||||||
if (quantUnits > 12)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 12; i++)
|
|
||||||
{
|
|
||||||
avg += sf[i];
|
|
||||||
}
|
|
||||||
avg = (avg + 6) / 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 8; i < quantUnits; i++)
|
|
||||||
{
|
|
||||||
int prevSf = sf[i - 1];
|
|
||||||
int nextSf = sf[i + 1];
|
|
||||||
int minSf = Math.Min(prevSf, nextSf);
|
|
||||||
if (sf[i] - minSf >= 3 || sf[i] - prevSf + sf[i] - nextSf >= 3)
|
|
||||||
{
|
|
||||||
channel.CodebookSet[i] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 12; i < quantUnits; i++)
|
|
||||||
{
|
|
||||||
if (channel.CodebookSet[i] == 0)
|
|
||||||
{
|
|
||||||
int minSf = Math.Min(sf[i - 1], sf[i + 1]);
|
|
||||||
if (sf[i] - minSf >= 2 && sf[i] >= avg - (Tables.QuantUnitToCoeffCount[i] == 16 ? 1 : 0))
|
|
||||||
{
|
|
||||||
channel.CodebookSet[i] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sf[quantUnits] = originalScaleTmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadSpectra(BitReader reader, Channel channel)
|
|
||||||
{
|
|
||||||
int[] values = channel.SpectraValuesBuffer;
|
|
||||||
Array.Clear(channel.QuantizedSpectra, 0, channel.QuantizedSpectra.Length);
|
|
||||||
int maxHuffPrecision = Tables.MaxHuffPrecision(channel.Config.HighSampleRate);
|
|
||||||
|
|
||||||
for (int i = 0; i < channel.CodedQuantUnits; i++)
|
|
||||||
{
|
|
||||||
int subbandCount = Tables.QuantUnitToCoeffCount[i];
|
|
||||||
int precision = channel.Precisions[i] + 1;
|
|
||||||
if (precision <= maxHuffPrecision)
|
|
||||||
{
|
|
||||||
HuffmanCodebook huff = Tables.HuffmanSpectrum[channel.CodebookSet[i]][precision][Tables.QuantUnitToCodebookIndex[i]];
|
|
||||||
int groupCount = subbandCount >> huff.ValueCountPower;
|
|
||||||
for (int j = 0; j < groupCount; j++)
|
|
||||||
{
|
|
||||||
values[j] = ReadHuffmanValue(huff, reader);
|
|
||||||
}
|
|
||||||
|
|
||||||
DecodeHuffmanValues(channel.QuantizedSpectra, Tables.QuantUnitToCoeffIndex[i], subbandCount, huff, values);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
int subbandIndex = Tables.QuantUnitToCoeffIndex[i];
|
|
||||||
for (int j = subbandIndex; j < Tables.QuantUnitToCoeffIndex[i + 1]; j++)
|
|
||||||
{
|
|
||||||
channel.QuantizedSpectra[j] = reader.ReadSignedInt(precision);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadSpectraFine(BitReader reader, Channel channel)
|
|
||||||
{
|
|
||||||
Array.Clear(channel.QuantizedSpectraFine, 0, channel.QuantizedSpectraFine.Length);
|
|
||||||
|
|
||||||
for (int i = 0; i < channel.CodedQuantUnits; i++)
|
|
||||||
{
|
|
||||||
if (channel.PrecisionsFine[i] > 0)
|
|
||||||
{
|
|
||||||
int overflowBits = channel.PrecisionsFine[i] + 1;
|
|
||||||
int startSubband = Tables.QuantUnitToCoeffIndex[i];
|
|
||||||
int endSubband = Tables.QuantUnitToCoeffIndex[i + 1];
|
|
||||||
|
|
||||||
for (int j = startSubband; j < endSubband; j++)
|
|
||||||
{
|
|
||||||
channel.QuantizedSpectraFine[j] = reader.ReadSignedInt(overflowBits);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DecodeHuffmanValues(int[] spectrum, int index, int bandCount, HuffmanCodebook huff, int[] values)
|
|
||||||
{
|
|
||||||
int valueCount = bandCount >> huff.ValueCountPower;
|
|
||||||
int mask = (1 << huff.ValueBits) - 1;
|
|
||||||
|
|
||||||
for (int i = 0; i < valueCount; i++)
|
|
||||||
{
|
|
||||||
int value = values[i];
|
|
||||||
for (int j = 0; j < huff.ValueCount; j++)
|
|
||||||
{
|
|
||||||
spectrum[index++] = Bit.SignExtend32(value & mask, huff.ValueBits);
|
|
||||||
value >>= huff.ValueBits;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int ReadHuffmanValue(HuffmanCodebook huff, BitReader reader, bool signed = false)
|
|
||||||
{
|
|
||||||
int code = reader.PeekInt(huff.MaxBitSize);
|
|
||||||
byte value = huff.Lookup[code];
|
|
||||||
int bits = huff.Bits[value];
|
|
||||||
reader.Position += bits;
|
|
||||||
return signed ? Bit.SignExtend32(value, huff.ValueBits) : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UnpackLfeBlock(BitReader reader, Block block)
|
|
||||||
{
|
|
||||||
Channel channel = block.Channels[0];
|
|
||||||
block.QuantizationUnitCount = 2;
|
|
||||||
|
|
||||||
DecodeLfeScaleFactors(reader, channel);
|
|
||||||
CalculateLfePrecision(channel);
|
|
||||||
channel.CodedQuantUnits = block.QuantizationUnitCount;
|
|
||||||
ReadLfeSpectra(reader, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DecodeLfeScaleFactors(BitReader reader, Channel channel)
|
|
||||||
{
|
|
||||||
Array.Clear(channel.ScaleFactors, 0, channel.ScaleFactors.Length);
|
|
||||||
for (int i = 0; i < channel.Block.QuantizationUnitCount; i++)
|
|
||||||
{
|
|
||||||
channel.ScaleFactors[i] = reader.ReadInt(5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void CalculateLfePrecision(Channel channel)
|
|
||||||
{
|
|
||||||
Block block = channel.Block;
|
|
||||||
int precision = block.ReuseBandParams ? 8 : 4;
|
|
||||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
|
||||||
{
|
|
||||||
channel.Precisions[i] = precision;
|
|
||||||
channel.PrecisionsFine[i] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadLfeSpectra(BitReader reader, Channel channel)
|
|
||||||
{
|
|
||||||
Array.Clear(channel.QuantizedSpectra, 0, channel.QuantizedSpectra.Length);
|
|
||||||
|
|
||||||
for (int i = 0; i < channel.CodedQuantUnits; i++)
|
|
||||||
{
|
|
||||||
if (channel.Precisions[i] <= 0) continue;
|
|
||||||
|
|
||||||
int precision = channel.Precisions[i] + 1;
|
|
||||||
for (int j = Tables.QuantUnitToCoeffIndex[i]; j < Tables.QuantUnitToCoeffIndex[i + 1]; j++)
|
|
||||||
{
|
|
||||||
channel.QuantizedSpectra[j] = reader.ReadSignedInt(precision);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
namespace LibAtrac9.Utilities
|
|
||||||
{
|
|
||||||
internal static class Bit
|
|
||||||
{
|
|
||||||
private static uint BitReverse32(uint value)
|
|
||||||
{
|
|
||||||
value = ((value & 0xaaaaaaaa) >> 1) | ((value & 0x55555555) << 1);
|
|
||||||
value = ((value & 0xcccccccc) >> 2) | ((value & 0x33333333) << 2);
|
|
||||||
value = ((value & 0xf0f0f0f0) >> 4) | ((value & 0x0f0f0f0f) << 4);
|
|
||||||
value = ((value & 0xff00ff00) >> 8) | ((value & 0x00ff00ff) << 8);
|
|
||||||
return (value >> 16) | (value << 16);
|
|
||||||
}
|
|
||||||
private static uint BitReverse32(uint value, int bitCount) => BitReverse32(value) >> (32 - bitCount);
|
|
||||||
public static int BitReverse32(int value, int bitCount) => (int) BitReverse32((uint) value, bitCount);
|
|
||||||
|
|
||||||
public static int SignExtend32(int value, int bits)
|
|
||||||
{
|
|
||||||
int shift = 8 * sizeof(int) - bits;
|
|
||||||
return (value << shift) >> shift;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace LibAtrac9.Utilities
|
|
||||||
{
|
|
||||||
internal class BitReader
|
|
||||||
{
|
|
||||||
private byte[] Buffer { get; set; }
|
|
||||||
private int LengthBits { get; set; }
|
|
||||||
public int Position { get; set; }
|
|
||||||
private int Remaining => LengthBits - Position;
|
|
||||||
|
|
||||||
public BitReader(byte[] buffer) => SetBuffer(buffer);
|
|
||||||
|
|
||||||
public void SetBuffer(byte[] buffer)
|
|
||||||
{
|
|
||||||
Buffer = buffer;
|
|
||||||
LengthBits = Buffer?.Length * 8 ?? 0;
|
|
||||||
Position = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int ReadInt(int bitCount)
|
|
||||||
{
|
|
||||||
int value = PeekInt(bitCount);
|
|
||||||
Position += bitCount;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int ReadSignedInt(int bitCount)
|
|
||||||
{
|
|
||||||
int value = PeekInt(bitCount);
|
|
||||||
Position += bitCount;
|
|
||||||
return Bit.SignExtend32(value, bitCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ReadBool() => ReadInt(1) == 1;
|
|
||||||
|
|
||||||
public int ReadOffsetBinary(int bitCount, OffsetBias bias)
|
|
||||||
{
|
|
||||||
int offset = (1 << (bitCount - 1)) - (int)bias;
|
|
||||||
int value = PeekInt(bitCount) - offset;
|
|
||||||
Position += bitCount;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AlignPosition(int multiple)
|
|
||||||
{
|
|
||||||
Position = Helpers.GetNextMultiple(Position, multiple);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int PeekInt(int bitCount)
|
|
||||||
{
|
|
||||||
Debug.Assert(bitCount >= 0 && bitCount <= 32);
|
|
||||||
|
|
||||||
if (bitCount > Remaining)
|
|
||||||
{
|
|
||||||
if (Position >= LengthBits) return 0;
|
|
||||||
|
|
||||||
int extraBits = bitCount - Remaining;
|
|
||||||
return PeekIntFallback(Remaining) << extraBits;
|
|
||||||
}
|
|
||||||
|
|
||||||
int byteIndex = Position / 8;
|
|
||||||
int bitIndex = Position % 8;
|
|
||||||
|
|
||||||
if (bitCount <= 9 && Remaining >= 16)
|
|
||||||
{
|
|
||||||
int value = Buffer[byteIndex] << 8 | Buffer[byteIndex + 1];
|
|
||||||
value &= 0xFFFF >> bitIndex;
|
|
||||||
value >>= 16 - bitCount - bitIndex;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bitCount <= 17 && Remaining >= 24)
|
|
||||||
{
|
|
||||||
int value = Buffer[byteIndex] << 16 | Buffer[byteIndex + 1] << 8 | Buffer[byteIndex + 2];
|
|
||||||
value &= 0xFFFFFF >> bitIndex;
|
|
||||||
value >>= 24 - bitCount - bitIndex;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bitCount <= 25 && Remaining >= 32)
|
|
||||||
{
|
|
||||||
int value = Buffer[byteIndex] << 24 | Buffer[byteIndex + 1] << 16 | Buffer[byteIndex + 2] << 8 | Buffer[byteIndex + 3];
|
|
||||||
value &= (int)(0xFFFFFFFF >> bitIndex);
|
|
||||||
value >>= 32 - bitCount - bitIndex;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return PeekIntFallback(bitCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int PeekIntFallback(int bitCount)
|
|
||||||
{
|
|
||||||
int value = 0;
|
|
||||||
int byteIndex = Position / 8;
|
|
||||||
int bitIndex = Position % 8;
|
|
||||||
|
|
||||||
while (bitCount > 0)
|
|
||||||
{
|
|
||||||
if (bitIndex >= 8)
|
|
||||||
{
|
|
||||||
bitIndex = 0;
|
|
||||||
byteIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
int bitsToRead = Math.Min(bitCount, 8 - bitIndex);
|
|
||||||
int mask = 0xFF >> bitIndex;
|
|
||||||
int currentByte = (mask & Buffer[byteIndex]) >> (8 - bitIndex - bitsToRead);
|
|
||||||
|
|
||||||
value = (value << bitsToRead) | currentByte;
|
|
||||||
bitIndex += bitsToRead;
|
|
||||||
bitCount -= bitsToRead;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Specifies the bias of an offset binary value. A positive bias can represent one more
|
|
||||||
/// positive value than negative value, and a negative bias can represent one more
|
|
||||||
/// negative value than positive value.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>Example:
|
|
||||||
/// A 4-bit offset binary value with a positive bias can store
|
|
||||||
/// the values 8 through -7 inclusive.
|
|
||||||
/// A 4-bit offset binary value with a positive bias can store
|
|
||||||
/// the values 7 through -8 inclusive.</remarks>
|
|
||||||
public enum OffsetBias
|
|
||||||
{
|
|
||||||
Negative = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace LibAtrac9.Utilities
|
|
||||||
{
|
|
||||||
internal static class Helpers
|
|
||||||
{
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public static short Clamp16(int value)
|
|
||||||
{
|
|
||||||
if (value > short.MaxValue)
|
|
||||||
return short.MaxValue;
|
|
||||||
if (value < short.MinValue)
|
|
||||||
return short.MinValue;
|
|
||||||
return (short)value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int GetNextMultiple(int value, int multiple)
|
|
||||||
{
|
|
||||||
if (multiple <= 0)
|
|
||||||
return value;
|
|
||||||
|
|
||||||
if (value % multiple == 0)
|
|
||||||
return value;
|
|
||||||
|
|
||||||
return value + multiple - value % multiple;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the floor of the base 2 logarithm of a specified number.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The number whose logarithm is to be found.</param>
|
|
||||||
/// <returns>The floor of the base 2 logarithm of <paramref name="value"/>.</returns>
|
|
||||||
public static int Log2(int value)
|
|
||||||
{
|
|
||||||
value |= value >> 1;
|
|
||||||
value |= value >> 2;
|
|
||||||
value |= value >> 4;
|
|
||||||
value |= value >> 8;
|
|
||||||
value |= value >> 16;
|
|
||||||
|
|
||||||
return MultiplyDeBruijnBitPosition[(uint)(value * 0x07C4ACDDU) >> 27];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly int[] MultiplyDeBruijnBitPosition =
|
|
||||||
{
|
|
||||||
0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30,
|
|
||||||
8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace LibAtrac9.Utilities
|
|
||||||
{
|
|
||||||
internal class Mdct
|
|
||||||
{
|
|
||||||
private int MdctBits { get; }
|
|
||||||
private int MdctSize { get; }
|
|
||||||
private double Scale { get; }
|
|
||||||
|
|
||||||
private static readonly object TableLock = new object();
|
|
||||||
private static int _tableBits = -1;
|
|
||||||
private static readonly List<double[]> SinTables = new List<double[]>();
|
|
||||||
private static readonly List<double[]> CosTables = new List<double[]>();
|
|
||||||
private static readonly List<int[]> ShuffleTables = new List<int[]>();
|
|
||||||
|
|
||||||
private readonly double[] _imdctPrevious;
|
|
||||||
private readonly double[] _imdctWindow;
|
|
||||||
|
|
||||||
private readonly double[] _scratchMdct;
|
|
||||||
private readonly double[] _scratchDct;
|
|
||||||
|
|
||||||
public Mdct(int mdctBits, double[] window, double scale = 1)
|
|
||||||
{
|
|
||||||
SetTables(mdctBits);
|
|
||||||
|
|
||||||
MdctBits = mdctBits;
|
|
||||||
MdctSize = 1 << mdctBits;
|
|
||||||
Scale = scale;
|
|
||||||
|
|
||||||
if (window.Length < MdctSize)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Window must be as long as the MDCT size.", nameof(window));
|
|
||||||
}
|
|
||||||
|
|
||||||
_imdctPrevious = new double[MdctSize];
|
|
||||||
_scratchMdct = new double[MdctSize];
|
|
||||||
_scratchDct = new double[MdctSize];
|
|
||||||
_imdctWindow = window;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SetTables(int maxBits)
|
|
||||||
{
|
|
||||||
lock (TableLock)
|
|
||||||
{
|
|
||||||
if (maxBits > _tableBits)
|
|
||||||
{
|
|
||||||
for (int i = _tableBits + 1; i <= maxBits; i++)
|
|
||||||
{
|
|
||||||
GenerateTrigTables(i, out double[] sin, out double[] cos);
|
|
||||||
SinTables.Add(sin);
|
|
||||||
CosTables.Add(cos);
|
|
||||||
ShuffleTables.Add(GenerateShuffleTable(i));
|
|
||||||
}
|
|
||||||
_tableBits = maxBits;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RunImdct(double[] input, double[] output)
|
|
||||||
{
|
|
||||||
if (input.Length < MdctSize)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Input must be as long as the MDCT size.", nameof(input));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (output.Length < MdctSize)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Output must be as long as the MDCT size.", nameof(output));
|
|
||||||
}
|
|
||||||
|
|
||||||
int size = MdctSize;
|
|
||||||
int half = size / 2;
|
|
||||||
double[] dctOut = _scratchMdct;
|
|
||||||
|
|
||||||
Dct4(input, dctOut);
|
|
||||||
|
|
||||||
for (int i = 0; i < half; i++)
|
|
||||||
{
|
|
||||||
output[i] = _imdctWindow[i] * dctOut[i + half] + _imdctPrevious[i];
|
|
||||||
output[i + half] = _imdctWindow[i + half] * -dctOut[size - 1 - i] - _imdctPrevious[i + half];
|
|
||||||
_imdctPrevious[i] = _imdctWindow[size - 1 - i] * -dctOut[half - i - 1];
|
|
||||||
_imdctPrevious[i + half] = _imdctWindow[half - i - 1] * dctOut[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Does a Type-4 DCT.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input">The input array containing the time or frequency-domain samples</param>
|
|
||||||
/// <param name="output">The output array that will contain the transformed time or frequency-domain samples</param>
|
|
||||||
private void Dct4(double[] input, double[] output)
|
|
||||||
{
|
|
||||||
int[] shuffleTable = ShuffleTables[MdctBits];
|
|
||||||
double[] sinTable = SinTables[MdctBits];
|
|
||||||
double[] cosTable = CosTables[MdctBits];
|
|
||||||
double[] dctTemp = _scratchDct;
|
|
||||||
|
|
||||||
int size = MdctSize;
|
|
||||||
int lastIndex = size - 1;
|
|
||||||
int halfSize = size / 2;
|
|
||||||
|
|
||||||
for (int i = 0; i < halfSize; i++)
|
|
||||||
{
|
|
||||||
int i2 = i * 2;
|
|
||||||
double a = input[i2];
|
|
||||||
double b = input[lastIndex - i2];
|
|
||||||
double sin = sinTable[i];
|
|
||||||
double cos = cosTable[i];
|
|
||||||
dctTemp[i2] = a * cos + b * sin;
|
|
||||||
dctTemp[i2 + 1] = a * sin - b * cos;
|
|
||||||
}
|
|
||||||
int stageCount = MdctBits - 1;
|
|
||||||
|
|
||||||
for (int stage = 0; stage < stageCount; stage++)
|
|
||||||
{
|
|
||||||
int blockCount = 1 << stage;
|
|
||||||
int blockSizeBits = stageCount - stage;
|
|
||||||
int blockHalfSizeBits = blockSizeBits - 1;
|
|
||||||
int blockSize = 1 << blockSizeBits;
|
|
||||||
int blockHalfSize = 1 << blockHalfSizeBits;
|
|
||||||
sinTable = SinTables[blockHalfSizeBits];
|
|
||||||
cosTable = CosTables[blockHalfSizeBits];
|
|
||||||
|
|
||||||
for (int block = 0; block < blockCount; block++)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < blockHalfSize; i++)
|
|
||||||
{
|
|
||||||
int frontPos = (block * blockSize + i) * 2;
|
|
||||||
int backPos = frontPos + blockSize;
|
|
||||||
double a = dctTemp[frontPos] - dctTemp[backPos];
|
|
||||||
double b = dctTemp[frontPos + 1] - dctTemp[backPos + 1];
|
|
||||||
double sin = sinTable[i];
|
|
||||||
double cos = cosTable[i];
|
|
||||||
dctTemp[frontPos] += dctTemp[backPos];
|
|
||||||
dctTemp[frontPos + 1] += dctTemp[backPos + 1];
|
|
||||||
dctTemp[backPos] = a * cos + b * sin;
|
|
||||||
dctTemp[backPos + 1] = a * sin - b * cos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < MdctSize; i++)
|
|
||||||
{
|
|
||||||
output[i] = dctTemp[shuffleTable[i]] * Scale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void GenerateTrigTables(int sizeBits, out double[] sin, out double[] cos)
|
|
||||||
{
|
|
||||||
int size = 1 << sizeBits;
|
|
||||||
sin = new double[size];
|
|
||||||
cos = new double[size];
|
|
||||||
|
|
||||||
for (int i = 0; i < size; i++)
|
|
||||||
{
|
|
||||||
double value = Math.PI * (4 * i + 1) / (4 * size);
|
|
||||||
sin[i] = Math.Sin(value);
|
|
||||||
cos[i] = Math.Cos(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static int[] GenerateShuffleTable(int sizeBits)
|
|
||||||
{
|
|
||||||
int size = 1 << sizeBits;
|
|
||||||
var table = new int[size];
|
|
||||||
|
|
||||||
for (int i = 0; i < size; i++)
|
|
||||||
{
|
|
||||||
table[i] = Bit.BitReverse32(i ^ (i / 2), sizeBits);
|
|
||||||
}
|
|
||||||
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +1,13 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System.ComponentModel;
|
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Media;
|
|
||||||
using Avalonia.Media.Imaging;
|
|
||||||
|
|
||||||
namespace SharpEmu.GUI;
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
public sealed class GameEntry : INotifyPropertyChanged
|
public sealed record GameEntry(string Name, string? TitleId, string Path, long SizeBytes)
|
||||||
{
|
{
|
||||||
// Placeholder gradients for games without cover art, picked
|
|
||||||
// deterministically from the game name so a game keeps its color.
|
|
||||||
private static readonly (Color Start, Color End)[] PlaceholderPalette =
|
|
||||||
{
|
|
||||||
(Color.Parse("#5B4B8A"), Color.Parse("#2C2A4A")),
|
|
||||||
(Color.Parse("#1F6E8C"), Color.Parse("#173B45")),
|
|
||||||
(Color.Parse("#7A4069"), Color.Parse("#3B1C32")),
|
|
||||||
(Color.Parse("#2D6A4F"), Color.Parse("#1B3A2B")),
|
|
||||||
(Color.Parse("#8C5425"), Color.Parse("#4A2B12")),
|
|
||||||
(Color.Parse("#4F6D9E"), Color.Parse("#263349")),
|
|
||||||
(Color.Parse("#8A4B4B"), Color.Parse("#3F2222")),
|
|
||||||
(Color.Parse("#3E7C7B"), Color.Parse("#1E3D3C")),
|
|
||||||
};
|
|
||||||
|
|
||||||
private Bitmap? _cover;
|
|
||||||
private IBrush? _placeholderBrush;
|
|
||||||
private long _sizeBytes;
|
|
||||||
|
|
||||||
public GameEntry(
|
|
||||||
string name, string? titleId, string path, long sizeBytes, string? coverPath, string? backgroundPath)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
TitleId = titleId;
|
|
||||||
Path = path;
|
|
||||||
_sizeBytes = sizeBytes;
|
|
||||||
CoverPath = coverPath;
|
|
||||||
BackgroundPath = backgroundPath;
|
|
||||||
Initials = ComputeInitials(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
|
|
||||||
public string Name { get; }
|
|
||||||
|
|
||||||
public string? TitleId { get; }
|
|
||||||
|
|
||||||
public string Path { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Total size of the game. Initially the eboot's own size from the scan;
|
|
||||||
/// replaced with the full install folder size once computed in the
|
|
||||||
/// background.
|
|
||||||
/// </summary>
|
|
||||||
public long SizeBytes
|
|
||||||
{
|
|
||||||
get => _sizeBytes;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_sizeBytes == value)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_sizeBytes = value;
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Detail)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Path to the cover art image shipped with the game, if found.</summary>
|
|
||||||
public string? CoverPath { get; }
|
|
||||||
|
|
||||||
/// <summary>Path to the key art (pic0/pic1) shipped with the game, if found.</summary>
|
|
||||||
public string? BackgroundPath { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decoded key art used as the window backdrop while this game is
|
|
||||||
/// selected. Loaded on demand and cached; not exposed via binding.
|
|
||||||
/// </summary>
|
|
||||||
public Bitmap? Background { get; set; }
|
|
||||||
|
|
||||||
public string Initials { get; }
|
|
||||||
|
|
||||||
// Built lazily: brushes are AvaloniaObjects that must be created on the
|
|
||||||
// UI thread, while GameEntry itself is constructed on the scan thread.
|
|
||||||
public IBrush PlaceholderBrush => _placeholderBrush ??= BuildPlaceholderBrush(Name);
|
|
||||||
|
|
||||||
/// <summary>Decoded cover art; loaded asynchronously after the library scan.</summary>
|
|
||||||
public Bitmap? Cover
|
|
||||||
{
|
|
||||||
get => _cover;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(_cover, value))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_cover = value;
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Cover)));
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasCover)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasCover => _cover is not null;
|
|
||||||
|
|
||||||
public string Detail => TitleId is not null
|
public string Detail => TitleId is not null
|
||||||
? $"{TitleId} • {FormatSize(SizeBytes)}"
|
? $"{TitleId} • {FormatSize(SizeBytes)}"
|
||||||
: FormatSize(SizeBytes);
|
: $"{FormatSize(SizeBytes)} • {Path}";
|
||||||
|
|
||||||
private static string ComputeInitials(string name)
|
|
||||||
{
|
|
||||||
var initials = name
|
|
||||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
||||||
.Where(word => char.IsLetterOrDigit(word[0]))
|
|
||||||
.Select(word => char.ToUpperInvariant(word[0]))
|
|
||||||
.Take(2)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return initials.Length > 0 ? new string(initials) : "?";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IBrush BuildPlaceholderBrush(string name)
|
|
||||||
{
|
|
||||||
var hash = 0;
|
|
||||||
foreach (var ch in name)
|
|
||||||
{
|
|
||||||
hash = unchecked(hash * 31 + ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
var (start, end) = PlaceholderPalette[(int)((uint)hash % PlaceholderPalette.Length)];
|
|
||||||
return new LinearGradientBrush
|
|
||||||
{
|
|
||||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
|
||||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
|
||||||
GradientStops =
|
|
||||||
{
|
|
||||||
new GradientStop(start, 0),
|
|
||||||
new GradientStop(end, 1),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatSize(long bytes)
|
private static string FormatSize(long bytes)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,21 +14,12 @@ public sealed class GuiSettings
|
|||||||
|
|
||||||
public List<string> GameFolders { get; set; } = new();
|
public List<string> GameFolders { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>Eboot paths hidden from the library via "Remove from library".</summary>
|
|
||||||
public List<string> ExcludedGames { get; set; } = new();
|
|
||||||
|
|
||||||
public string LogLevel { get; set; } = "Info";
|
public string LogLevel { get; set; } = "Info";
|
||||||
|
|
||||||
public int ImportTraceLimit { get; set; }
|
public int ImportTraceLimit { get; set; }
|
||||||
|
|
||||||
public bool StrictDynlibResolution { get; set; }
|
public bool StrictDynlibResolution { get; set; }
|
||||||
|
|
||||||
/// <summary>Mirror emulator output to user/logs/<titleId>-<timestamp>.log.</summary>
|
|
||||||
public bool LogToFile { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Loop the selected game's sce_sys/snd0.at9 preview music.</summary>
|
|
||||||
public bool PlayTitleMusic { get; set; } = true;
|
|
||||||
|
|
||||||
public string? EmulatorPath { get; set; }
|
public string? EmulatorPath { get; set; }
|
||||||
|
|
||||||
// The emulator is portable and keeps its data next to the executable;
|
// The emulator is portable and keeps its data next to the executable;
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
x:Class="SharpEmu.GUI.MainWindow"
|
x:Class="SharpEmu.GUI.MainWindow"
|
||||||
Title="SharpEmu"
|
Title="SharpEmu"
|
||||||
Width="1280" Height="820"
|
Width="1280" Height="800"
|
||||||
MinWidth="980" MinHeight="640"
|
MinWidth="980" MinHeight="620"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
Background="{StaticResource BgBrush}"
|
Background="{StaticResource BgBrush}"
|
||||||
ExtendClientAreaToDecorationsHint="True"
|
ExtendClientAreaToDecorationsHint="True"
|
||||||
@@ -18,33 +18,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<Grid RowDefinitions="44,*,32">
|
<Grid RowDefinitions="44,*,32">
|
||||||
|
|
||||||
<!-- Selected-game backdrop: key art behind the main content, dimmed by
|
|
||||||
a scrim so tiles and text stay readable. Fades on selection. -->
|
|
||||||
<Panel Grid.Row="1" ClipToBounds="True">
|
|
||||||
<Image x:Name="BackdropImage" Stretch="UniformToFill" Opacity="0"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
|
||||||
<Image.Transitions>
|
|
||||||
<Transitions>
|
|
||||||
<DoubleTransition Property="Opacity" Duration="0:0:0.35" />
|
|
||||||
</Transitions>
|
|
||||||
</Image.Transitions>
|
|
||||||
</Image>
|
|
||||||
<Border>
|
|
||||||
<Border.Background>
|
|
||||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
|
|
||||||
<GradientStop Offset="0" Color="#730D1017" />
|
|
||||||
<GradientStop Offset="0.55" Color="#BF0D1017" />
|
|
||||||
<GradientStop Offset="1" Color="#EB0D1017" />
|
|
||||||
</LinearGradientBrush>
|
|
||||||
</Border.Background>
|
|
||||||
</Border>
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<!-- Title bar -->
|
<!-- Title bar -->
|
||||||
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
|
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
||||||
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
|
<Image Source="avares://SharpEmu.GUI/Assets/logo.png" Width="20" Height="20" />
|
||||||
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
|
||||||
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||||
<Border Classes="pill">
|
<Border Classes="pill">
|
||||||
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||||
@@ -53,212 +30,119 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<Grid Grid.Row="1" Margin="18,14,18,14" RowDefinitions="Auto,*,Auto,Auto">
|
<Grid Grid.Row="1" Margin="14" ColumnDefinitions="360,14,*">
|
||||||
|
|
||||||
<!-- Library toolbar -->
|
<!-- Game library -->
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto" Margin="6,0,6,12">
|
<Border Grid.Column="0" Classes="card" Padding="0">
|
||||||
<TextBlock Grid.Column="0" Text="Library" FontSize="22" FontWeight="Bold" VerticalAlignment="Center" />
|
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||||
<Border Grid.Column="1" Classes="pill" Margin="12,2,0,0" VerticalAlignment="Center">
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="16,16,16,10">
|
||||||
<TextBlock x:Name="GameCountText" Text="0 games" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
<TextBlock Classes="sectionTitle" Text="GAME LIBRARY" VerticalAlignment="Center" />
|
||||||
</Border>
|
<Border Grid.Column="1" Classes="pill">
|
||||||
<TextBox Grid.Column="3" x:Name="SearchBox" Watermark="Search library…" Width="280"
|
<TextBlock x:Name="GameCountText" Text="0" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||||
VerticalAlignment="Center" Margin="0,0,12,0" />
|
</Border>
|
||||||
<Button Grid.Column="4" x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder"
|
|
||||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
|
||||||
<Button Grid.Column="5" x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan"
|
|
||||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
|
||||||
<Button Grid.Column="6" x:Name="OpenFileButton" Classes="ghost" Content="Open file…"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Cover-art grid -->
|
|
||||||
<Panel Grid.Row="1">
|
|
||||||
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
|
|
||||||
SelectionMode="Single" Padding="0">
|
|
||||||
<ListBox.ContextMenu>
|
|
||||||
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
|
|
||||||
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
<Separator />
|
|
||||||
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
<Separator />
|
|
||||||
<MenuItem x:Name="CtxRemove" Header="Remove from library"
|
|
||||||
Foreground="{StaticResource DangerHoverBrush}">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
</ContextMenu>
|
|
||||||
</ListBox.ContextMenu>
|
|
||||||
<ListBox.ItemsPanel>
|
|
||||||
<ItemsPanelTemplate>
|
|
||||||
<WrapPanel />
|
|
||||||
</ItemsPanelTemplate>
|
|
||||||
</ListBox.ItemsPanel>
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<StackPanel Width="128" Height="186" Spacing="7">
|
|
||||||
<Border Classes="coverShadow" Width="128" Height="128">
|
|
||||||
<Border Classes="coverClip">
|
|
||||||
<Panel>
|
|
||||||
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
|
|
||||||
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
|
|
||||||
Foreground="#E8ECF4" Opacity="0.85"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</Border>
|
|
||||||
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
|
|
||||||
</Panel>
|
|
||||||
</Border>
|
|
||||||
</Border>
|
|
||||||
<StackPanel Spacing="2">
|
|
||||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
|
|
||||||
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis" />
|
|
||||||
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}"
|
|
||||||
TextTrimming="CharacterEllipsis" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
|
|
||||||
<!-- Empty state -->
|
|
||||||
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
|
|
||||||
IsVisible="False">
|
|
||||||
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
|
|
||||||
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
|
|
||||||
HorizontalAlignment="Center" />
|
|
||||||
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
|
|
||||||
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
|
||||||
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content="+ Add game folder"
|
|
||||||
HorizontalAlignment="Center" Margin="0,8,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<!-- Console (collapsible) -->
|
|
||||||
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
|
|
||||||
Margin="0,12,0,0" IsVisible="False">
|
|
||||||
<Grid RowDefinitions="Auto,*">
|
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,12,16,8">
|
|
||||||
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
|
||||||
<CheckBox Grid.Column="1" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
|
||||||
FontSize="12" Margin="0,0,12,0" />
|
|
||||||
<Button Grid.Column="2" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
|
||||||
Padding="10,4" Margin="0,0,8,0" />
|
|
||||||
<Button Grid.Column="3" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
|
|
||||||
Padding="10,4" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
|
||||||
BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,12,12">
|
<TextBox Grid.Row="1" x:Name="SearchBox" Watermark="Search games…" Margin="12,0,12,8" CornerRadius="8" />
|
||||||
|
|
||||||
|
<ListBox Grid.Row="2" x:Name="GameList" Classes="library" Background="Transparent" Margin="8,0">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
|
<StackPanel Margin="4,4" Spacing="2">
|
||||||
|
<TextBlock Text="{Binding Name}" FontSize="14" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
|
||||||
|
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||||
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="8" Margin="16,12,16,16">
|
||||||
|
<Button x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder" />
|
||||||
|
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" />
|
||||||
|
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" />
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Launch bar -->
|
<!-- Right column -->
|
||||||
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14">
|
<Grid Grid.Column="2" RowDefinitions="Auto,12,Auto,12,*">
|
||||||
<StackPanel Spacing="14">
|
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
||||||
|
|
||||||
<!-- Selected game cover thumbnail -->
|
<!-- Selected game / launch -->
|
||||||
<Border Grid.Column="0" Classes="coverClip" Width="56" Height="56" CornerRadius="8"
|
<Border Grid.Row="0" Classes="card">
|
||||||
VerticalAlignment="Center">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<Panel x:Name="SelectedCoverPanel">
|
<StackPanel Grid.Column="0" Spacing="6" VerticalAlignment="Center">
|
||||||
<Border Background="{Binding PlaceholderBrush, FallbackValue={x:Null}}"
|
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="20" FontWeight="Bold" TextTrimming="CharacterEllipsis" />
|
||||||
IsVisible="{Binding !HasCover, FallbackValue=False}">
|
|
||||||
<TextBlock Text="{Binding Initials}" FontSize="20" FontWeight="Bold"
|
|
||||||
Foreground="#E8ECF4" Opacity="0.85"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</Border>
|
|
||||||
<Image Source="{Binding Cover}" Stretch="UniformToFill"
|
|
||||||
IsVisible="{Binding HasCover, FallbackValue=False}" />
|
|
||||||
</Panel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" Spacing="3" VerticalAlignment="Center" Margin="14,0,14,0">
|
|
||||||
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="16" FontWeight="Bold"
|
|
||||||
TextTrimming="CharacterEllipsis" />
|
|
||||||
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
|
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
|
||||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
FontSize="12" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
<Ellipse x:Name="StatusDot" Width="8" Height="8" Fill="{StaticResource FaintBrush}"
|
<Ellipse x:Name="StatusDot" Width="9" Height="9" Fill="{StaticResource FaintBrush}" VerticalAlignment="Center" />
|
||||||
VerticalAlignment="Center" />
|
<TextBlock x:Name="StatusText" Text="Idle" FontSize="12" Foreground="{StaticResource MutedBrush}" VerticalAlignment="Center" />
|
||||||
<TextBlock x:Name="StatusText" Text="Idle" FontSize="11" Foreground="{StaticResource MutedBrush}"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center" Margin="16,0,0,0">
|
||||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
|
||||||
<ToggleButton x:Name="OptionsToggle" Classes="ghost" Content="⚙ Options" />
|
|
||||||
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
|
|
||||||
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
|
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
|
||||||
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Launch options (collapsible) -->
|
<!-- Launch options -->
|
||||||
<WrapPanel x:Name="OptionsPanel" IsVisible="False">
|
<Border Grid.Row="2" Classes="card">
|
||||||
<StackPanel Margin="0,0,24,0">
|
<StackPanel Spacing="12">
|
||||||
<TextBlock Classes="fieldLabel" Text="CPU engine" />
|
<TextBlock Classes="sectionTitle" Text="LAUNCH OPTIONS" />
|
||||||
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
|
<WrapPanel>
|
||||||
<ComboBoxItem Content="Native" />
|
<StackPanel Margin="0,0,24,0">
|
||||||
</ComboBox>
|
<TextBlock Classes="fieldLabel" Text="CPU engine" />
|
||||||
</StackPanel>
|
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
|
||||||
<StackPanel Margin="0,0,24,0">
|
<ComboBoxItem Content="Native" />
|
||||||
<TextBlock Classes="fieldLabel" Text="Log level" />
|
</ComboBox>
|
||||||
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
|
</StackPanel>
|
||||||
<ComboBoxItem Content="Trace" />
|
<StackPanel Margin="0,0,24,0">
|
||||||
<ComboBoxItem Content="Debug" />
|
<TextBlock Classes="fieldLabel" Text="Log level" />
|
||||||
<ComboBoxItem Content="Info" />
|
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
|
||||||
<ComboBoxItem Content="Warning" />
|
<ComboBoxItem Content="Trace" />
|
||||||
<ComboBoxItem Content="Error" />
|
<ComboBoxItem Content="Debug" />
|
||||||
<ComboBoxItem Content="Critical" />
|
<ComboBoxItem Content="Info" />
|
||||||
</ComboBox>
|
<ComboBoxItem Content="Warning" />
|
||||||
</StackPanel>
|
<ComboBoxItem Content="Error" />
|
||||||
<StackPanel Margin="0,0,24,0">
|
<ComboBoxItem Content="Critical" />
|
||||||
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
|
</ComboBox>
|
||||||
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
|
</StackPanel>
|
||||||
Value="0" FormatString="0" CornerRadius="8" />
|
<StackPanel Margin="0,0,24,0">
|
||||||
</StackPanel>
|
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
|
||||||
<StackPanel Margin="0,0,24,0">
|
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
|
||||||
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
|
Value="0" FormatString="0" CornerRadius="8" />
|
||||||
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
|
</StackPanel>
|
||||||
</StackPanel>
|
<StackPanel>
|
||||||
<StackPanel Margin="0,0,24,0">
|
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
|
||||||
<TextBlock Classes="fieldLabel" Text="Log to file" />
|
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
|
||||||
<ToggleSwitch x:Name="LogToFileToggle" OnContent="On" OffContent="Off" />
|
</StackPanel>
|
||||||
</StackPanel>
|
</WrapPanel>
|
||||||
<StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Classes="fieldLabel" Text="Title music" />
|
</Border>
|
||||||
<ToggleSwitch x:Name="TitleMusicToggle" OnContent="On" OffContent="Off" IsChecked="True" />
|
|
||||||
</StackPanel>
|
<!-- Console -->
|
||||||
</WrapPanel>
|
<Border Grid.Row="4" Classes="card" Padding="0">
|
||||||
</StackPanel>
|
<Grid RowDefinitions="Auto,*">
|
||||||
</Border>
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,14,16,10">
|
||||||
|
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
||||||
|
<CheckBox Grid.Column="1" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
||||||
|
FontSize="12" Margin="0,0,12,0" />
|
||||||
|
<Button Grid.Column="2" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
||||||
|
Padding="10,4" Margin="0,0,8,0" />
|
||||||
|
<Button Grid.Column="3" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
|
||||||
|
Padding="10,4" />
|
||||||
|
</Grid>
|
||||||
|
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
||||||
|
BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,10,10">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Status bar -->
|
<!-- Status bar -->
|
||||||
|
|||||||
@@ -5,18 +5,11 @@ using System.Collections.Concurrent;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Diagnostics;
|
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Media.Imaging;
|
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Avalonia.VisualTree;
|
|
||||||
using SharpEmu.Libs.Pad;
|
|
||||||
using SharpEmu.Logging;
|
|
||||||
|
|
||||||
namespace SharpEmu.GUI;
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
@@ -39,21 +32,9 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private GuiSettings _settings = new();
|
private GuiSettings _settings = new();
|
||||||
private EmulatorProcess? _emulator;
|
private EmulatorProcess? _emulator;
|
||||||
private StreamWriter? _fileLog;
|
|
||||||
private readonly SndPreviewPlayer _sndPreview = new();
|
|
||||||
private string? _emulatorExePath;
|
private string? _emulatorExePath;
|
||||||
private bool _isRunning;
|
private bool _isRunning;
|
||||||
private int _autoScrollTicks;
|
private int _autoScrollTicks;
|
||||||
private int _detailLoadGeneration;
|
|
||||||
private int _backdropGeneration;
|
|
||||||
|
|
||||||
// Controller navigation state.
|
|
||||||
private readonly DispatcherTimer _gamepadTimer;
|
|
||||||
private uint _previousPadButtons;
|
|
||||||
private long _navLeftNextAt;
|
|
||||||
private long _navRightNextAt;
|
|
||||||
private long _navUpNextAt;
|
|
||||||
private long _navDownNextAt;
|
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
@@ -78,159 +59,24 @@ public partial class MainWindow : Window
|
|||||||
GameList.DoubleTapped += (_, _) => LaunchSelected();
|
GameList.DoubleTapped += (_, _) => LaunchSelected();
|
||||||
SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
|
SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
|
||||||
AddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
AddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
||||||
EmptyAddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
|
||||||
RescanButton.Click += async (_, _) => await RescanLibraryAsync();
|
RescanButton.Click += async (_, _) => await RescanLibraryAsync();
|
||||||
OpenFileButton.Click += async (_, _) => await OpenFileAsync();
|
OpenFileButton.Click += async (_, _) => await OpenFileAsync();
|
||||||
LaunchButton.Click += (_, _) => LaunchSelected();
|
LaunchButton.Click += (_, _) => LaunchSelected();
|
||||||
StopButton.Click += (_, _) => _emulator?.Stop();
|
StopButton.Click += (_, _) => _emulator?.Stop();
|
||||||
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
|
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
|
||||||
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
|
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
|
||||||
OptionsToggle.IsCheckedChanged += (_, _) => OptionsPanel.IsVisible = OptionsToggle.IsChecked == true;
|
|
||||||
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true;
|
|
||||||
TitleMusicToggle.IsCheckedChanged += (_, _) => OnTitleMusicToggled();
|
|
||||||
|
|
||||||
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
|
||||||
CtxLaunch.Click += (_, _) => LaunchSelected();
|
|
||||||
CtxOpenFolder.Click += (_, _) => OpenSelectedGameFolder();
|
|
||||||
CtxCopyPath.Click += async (_, _) =>
|
|
||||||
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path, "Path");
|
|
||||||
CtxCopyTitleId.Click += async (_, _) =>
|
|
||||||
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId, "Title ID");
|
|
||||||
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
|
|
||||||
|
|
||||||
Opened += async (_, _) => await OnOpenedAsync();
|
Opened += async (_, _) => await OnOpenedAsync();
|
||||||
Closing += (_, _) => OnWindowClosing();
|
Closing += (_, _) => OnWindowClosing();
|
||||||
|
|
||||||
DualSenseReader.EnsureStarted();
|
|
||||||
XInputReader.EnsureStarted();
|
|
||||||
_gamepadTimer = new DispatcherTimer
|
|
||||||
{
|
|
||||||
Interval = TimeSpan.FromMilliseconds(50),
|
|
||||||
};
|
|
||||||
_gamepadTimer.Tick += (_, _) => PollGamepad();
|
|
||||||
_gamepadTimer.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Controller navigation ----
|
|
||||||
|
|
||||||
private void PollGamepad()
|
|
||||||
{
|
|
||||||
// DualSense wins when both are connected; XInput covers Xbox pads.
|
|
||||||
if (!DualSenseReader.TryGetState(out var pad) && !XInputReader.TryGetState(out pad))
|
|
||||||
{
|
|
||||||
_previousPadButtons = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsActive)
|
|
||||||
{
|
|
||||||
// Ignore input while the launcher is in the background (e.g. the
|
|
||||||
// game window is focused and using the same controller).
|
|
||||||
_previousPadButtons = pad.Buttons;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = Environment.TickCount64;
|
|
||||||
var left = (pad.Buttons & 0x0080) != 0 || pad.LeftX < 64;
|
|
||||||
var right = (pad.Buttons & 0x0020) != 0 || pad.LeftX > 192;
|
|
||||||
var up = (pad.Buttons & 0x0010) != 0 || pad.LeftY < 64;
|
|
||||||
var down = (pad.Buttons & 0x0040) != 0 || pad.LeftY > 192;
|
|
||||||
|
|
||||||
if (ShouldNavigate(left, ref _navLeftNextAt, now))
|
|
||||||
{
|
|
||||||
MoveSelection(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ShouldNavigate(right, ref _navRightNextAt, now))
|
|
||||||
{
|
|
||||||
MoveSelection(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ShouldNavigate(up, ref _navUpNextAt, now))
|
|
||||||
{
|
|
||||||
MoveSelection(-TilesPerRow());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ShouldNavigate(down, ref _navDownNextAt, now))
|
|
||||||
{
|
|
||||||
MoveSelection(TilesPerRow());
|
|
||||||
}
|
|
||||||
|
|
||||||
var pressed = pad.Buttons & ~_previousPadButtons;
|
|
||||||
if ((pressed & 0x4000) != 0) // Cross
|
|
||||||
{
|
|
||||||
LaunchSelected();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((pressed & 0x2000) != 0) // Circle
|
|
||||||
{
|
|
||||||
_emulator?.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
_previousPadButtons = pad.Buttons;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Edge-triggered with hold-to-repeat: fires on press, then repeats
|
|
||||||
/// after 400ms at 130ms intervals while held.
|
|
||||||
/// </summary>
|
|
||||||
private static bool ShouldNavigate(bool held, ref long nextAt, long now)
|
|
||||||
{
|
|
||||||
if (!held)
|
|
||||||
{
|
|
||||||
nextAt = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextAt == 0)
|
|
||||||
{
|
|
||||||
nextAt = now + 400;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (now >= nextAt)
|
|
||||||
{
|
|
||||||
nextAt = now + 130;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MoveSelection(int delta)
|
|
||||||
{
|
|
||||||
if (_visibleGames.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = GameList.SelectedIndex < 0
|
|
||||||
? 0
|
|
||||||
: Math.Clamp(GameList.SelectedIndex + delta, 0, _visibleGames.Count - 1);
|
|
||||||
GameList.SelectedIndex = index;
|
|
||||||
GameList.ScrollIntoView(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int TilesPerRow()
|
|
||||||
{
|
|
||||||
// Tile footprint: 128 content + 20 item padding + 10 item margin.
|
|
||||||
const double TileOuterWidth = 158;
|
|
||||||
var width = GameList.Bounds.Width;
|
|
||||||
return width > TileOuterWidth ? (int)(width / TileOuterWidth) : 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnOpenedAsync()
|
private async Task OnOpenedAsync()
|
||||||
{
|
{
|
||||||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
var display = version is not null ? $"v{version.ToString(3)}" : "v0.0.1";
|
if (version is not null)
|
||||||
display += BuildInfo.CommitSha is null
|
{
|
||||||
? " · dev"
|
VersionText.Text = $"v{version.ToString(3)}";
|
||||||
: BuildInfo.IsOfficialRelease
|
}
|
||||||
? $" · {BuildInfo.CommitSha}"
|
|
||||||
: $" · UNOFFICIAL {BuildInfo.CommitSha}";
|
|
||||||
VersionText.Text = display;
|
|
||||||
Title = $"SharpEmu {display}";
|
|
||||||
ToolTip.SetTip(VersionText, BuildInfo.Banner);
|
|
||||||
|
|
||||||
_settings = GuiSettings.Load();
|
_settings = GuiSettings.Load();
|
||||||
ApplySettingsToControls();
|
ApplySettingsToControls();
|
||||||
@@ -243,10 +89,7 @@ public partial class MainWindow : Window
|
|||||||
ReadControlsIntoSettings();
|
ReadControlsIntoSettings();
|
||||||
_settings.Save();
|
_settings.Save();
|
||||||
_consoleFlushTimer.Stop();
|
_consoleFlushTimer.Stop();
|
||||||
_gamepadTimer.Stop();
|
|
||||||
_sndPreview.Stop();
|
|
||||||
_emulator?.Dispose();
|
_emulator?.Dispose();
|
||||||
DropFileLog();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
|
private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
@@ -273,8 +116,6 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
||||||
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
||||||
LogToFileToggle.IsChecked = _settings.LogToFile;
|
|
||||||
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReadControlsIntoSettings()
|
private void ReadControlsIntoSettings()
|
||||||
@@ -282,8 +123,6 @@ public partial class MainWindow : Window
|
|||||||
_settings.LogLevel = SelectedLogLevel();
|
_settings.LogLevel = SelectedLogLevel();
|
||||||
_settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
_settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||||
_settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
_settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||||
_settings.LogToFile = LogToFileToggle.IsChecked == true;
|
|
||||||
_settings.PlayTitleMusic = TitleMusicToggle.IsChecked == true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string SelectedLogLevel()
|
private string SelectedLogLevel()
|
||||||
@@ -349,21 +188,9 @@ public partial class MainWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var changed = false;
|
|
||||||
if (!_settings.GameFolders.Contains(path, StringComparer.OrdinalIgnoreCase))
|
if (!_settings.GameFolders.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_settings.GameFolders.Add(path);
|
_settings.GameFolders.Add(path);
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adding (or re-adding) a folder is an explicit signal to restore any
|
|
||||||
// games beneath it that were removed from the library earlier.
|
|
||||||
var prefix = Path.TrimEndingDirectorySeparator(path) + Path.DirectorySeparatorChar;
|
|
||||||
changed |= _settings.ExcludedGames.RemoveAll(excluded =>
|
|
||||||
excluded.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) > 0;
|
|
||||||
|
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
_settings.Save();
|
_settings.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,118 +200,19 @@ public partial class MainWindow : Window
|
|||||||
private async Task RescanLibraryAsync()
|
private async Task RescanLibraryAsync()
|
||||||
{
|
{
|
||||||
var folders = _settings.GameFolders.ToArray();
|
var folders = _settings.GameFolders.ToArray();
|
||||||
var excluded = new HashSet<string>(_settings.ExcludedGames, StringComparer.OrdinalIgnoreCase);
|
|
||||||
StatusBarRight.Text = "Scanning library…";
|
StatusBarRight.Text = "Scanning library…";
|
||||||
|
|
||||||
var games = await Task.Run(() => ScanFolders(folders, excluded));
|
var games = await Task.Run(() => ScanFolders(folders));
|
||||||
|
|
||||||
_allGames.Clear();
|
_allGames.Clear();
|
||||||
_allGames.AddRange(games);
|
_allGames.AddRange(games);
|
||||||
RefreshVisibleGames();
|
RefreshVisibleGames();
|
||||||
LoadGameDetailsInBackground(games);
|
|
||||||
StatusBarRight.Text = folders.Length == 0
|
StatusBarRight.Text = folders.Length == 0
|
||||||
? "Add a game folder to populate the library."
|
? "Add a game folder to populate the library."
|
||||||
: $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s).";
|
: $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s).";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders)
|
||||||
/// Enriches games off the UI thread — decodes cover art and totals each
|
|
||||||
/// game's install folder size — posting results back as they become
|
|
||||||
/// ready. A newer scan invalidates older loads.
|
|
||||||
/// </summary>
|
|
||||||
private void LoadGameDetailsInBackground(IReadOnlyList<GameEntry> games)
|
|
||||||
{
|
|
||||||
var generation = ++_detailLoadGeneration;
|
|
||||||
_ = Task.Run(() =>
|
|
||||||
{
|
|
||||||
// Covers first: they are cheap and the most visible, so the grid
|
|
||||||
// fills with art before the (potentially slow) size pass runs.
|
|
||||||
foreach (var game in games)
|
|
||||||
{
|
|
||||||
if (generation != _detailLoadGeneration)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game.CoverPath is null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var stream = File.OpenRead(game.CoverPath);
|
|
||||||
var bitmap = Bitmap.DecodeToWidth(stream, 312);
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
if (generation == _detailLoadGeneration)
|
|
||||||
{
|
|
||||||
game.Cover = bitmap;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// A missing or undecodable image keeps the placeholder.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var game in games)
|
|
||||||
{
|
|
||||||
if (generation != _detailLoadGeneration)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var size = ComputeInstallSize(game.Path);
|
|
||||||
if (size > 0)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
if (generation == _detailLoadGeneration)
|
|
||||||
{
|
|
||||||
game.SizeBytes = size;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Totals the size of the game's install folder (the directory holding
|
|
||||||
/// the eboot), which is far more accurate than the eboot alone.
|
|
||||||
/// </summary>
|
|
||||||
private static long ComputeInstallSize(string ebootPath)
|
|
||||||
{
|
|
||||||
var directory = Path.GetDirectoryName(ebootPath);
|
|
||||||
if (directory is null)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
long total = 0;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var enumeration = new EnumerationOptions
|
|
||||||
{
|
|
||||||
IgnoreInaccessible = true,
|
|
||||||
RecurseSubdirectories = true,
|
|
||||||
};
|
|
||||||
foreach (var file in new DirectoryInfo(directory).EnumerateFiles("*", enumeration))
|
|
||||||
{
|
|
||||||
total += file.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// Fall back to whatever was accumulated so far.
|
|
||||||
}
|
|
||||||
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders, IReadOnlySet<string> excludedPaths)
|
|
||||||
{
|
{
|
||||||
var games = new List<GameEntry>();
|
var games = new List<GameEntry>();
|
||||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -507,7 +235,7 @@ public partial class MainWindow : Window
|
|||||||
foreach (var file in Directory.EnumerateFiles(folder, "eboot.bin", enumeration))
|
foreach (var file in Directory.EnumerateFiles(folder, "eboot.bin", enumeration))
|
||||||
{
|
{
|
||||||
var fullPath = Path.GetFullPath(file);
|
var fullPath = Path.GetFullPath(file);
|
||||||
if (!seen.Add(fullPath) || excludedPaths.Contains(fullPath))
|
if (!seen.Add(fullPath))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -522,9 +250,7 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (title, titleId) = TryReadParamJson(fullPath);
|
var (title, titleId) = TryReadParamJson(fullPath);
|
||||||
games.Add(new GameEntry(
|
games.Add(new GameEntry(title ?? GameNameFor(fullPath), titleId, fullPath, size));
|
||||||
title ?? GameNameFor(fullPath), titleId, fullPath, size,
|
|
||||||
FindCoverFor(fullPath), FindBackgroundFor(fullPath)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@@ -606,56 +332,6 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds the cover art shipped with the game: sce_sys/icon0.png next to
|
|
||||||
/// the executable (falling back to pic0.png).
|
|
||||||
/// </summary>
|
|
||||||
private static string? FindCoverFor(string ebootPath)
|
|
||||||
{
|
|
||||||
var directory = Path.GetDirectoryName(ebootPath);
|
|
||||||
if (directory is null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sceSys = Path.Combine(directory, "sce_sys");
|
|
||||||
foreach (var candidate in new[] { "icon0.png", "pic0.png" })
|
|
||||||
{
|
|
||||||
var coverPath = Path.Combine(sceSys, candidate);
|
|
||||||
if (File.Exists(coverPath))
|
|
||||||
{
|
|
||||||
return coverPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds the key art shipped with the game (sce_sys/pic0.png, falling
|
|
||||||
/// back to pic1.png), used as the window backdrop when selected.
|
|
||||||
/// </summary>
|
|
||||||
private static string? FindBackgroundFor(string ebootPath)
|
|
||||||
{
|
|
||||||
var directory = Path.GetDirectoryName(ebootPath);
|
|
||||||
if (directory is null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sceSys = Path.Combine(directory, "sce_sys");
|
|
||||||
foreach (var candidate in new[] { "pic0.png", "pic1.png" })
|
|
||||||
{
|
|
||||||
var backgroundPath = Path.Combine(sceSys, candidate);
|
|
||||||
if (File.Exists(backgroundPath))
|
|
||||||
{
|
|
||||||
return backgroundPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GameNameFor(string ebootPath)
|
private static string GameNameFor(string ebootPath)
|
||||||
{
|
{
|
||||||
var directory = Path.GetDirectoryName(ebootPath);
|
var directory = Path.GetDirectoryName(ebootPath);
|
||||||
@@ -663,125 +339,26 @@ public partial class MainWindow : Window
|
|||||||
return string.IsNullOrEmpty(name) ? Path.GetFileName(ebootPath) : name;
|
return string.IsNullOrEmpty(name) ? Path.GetFileName(ebootPath) : name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Game context menu ----
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Selects the tile under the pointer before its context menu opens, and
|
|
||||||
/// suppresses the menu on empty grid space.
|
|
||||||
/// </summary>
|
|
||||||
private void OnGameContextRequested(object? sender, ContextRequestedEventArgs e)
|
|
||||||
{
|
|
||||||
var item = (e.Source as Visual)?.FindAncestorOfType<ListBoxItem>(includeSelf: true);
|
|
||||||
if (item?.DataContext is not GameEntry game)
|
|
||||||
{
|
|
||||||
e.Handled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GameList.SelectedItem = game;
|
|
||||||
CtxLaunch.IsEnabled = !_isRunning;
|
|
||||||
CtxCopyTitleId.IsEnabled = game.TitleId is not null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OpenSelectedGameFolder()
|
|
||||||
{
|
|
||||||
if (GameList.SelectedItem is not GameEntry game)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
Process.Start(new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "explorer.exe",
|
|
||||||
Arguments = $"/select,\"{game.Path}\"",
|
|
||||||
UseShellExecute = false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (Path.GetDirectoryName(game.Path) is { } directory)
|
|
||||||
{
|
|
||||||
Process.Start(new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = OperatingSystem.IsMacOS() ? "open" : "xdg-open",
|
|
||||||
Arguments = $"\"{directory}\"",
|
|
||||||
UseShellExecute = false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
StatusBarRight.Text = $"Could not open folder: {ex.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CopyToClipboardAsync(string? text, string what)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(text) || Clipboard is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Clipboard.SetTextAsync(text);
|
|
||||||
StatusBarRight.Text = $"{what} copied to clipboard.";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveSelectedFromLibrary()
|
|
||||||
{
|
|
||||||
if (GameList.SelectedItem is not GameEntry game)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_settings.ExcludedGames.Contains(game.Path, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
_settings.ExcludedGames.Add(game.Path);
|
|
||||||
_settings.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, StringComparison.OrdinalIgnoreCase));
|
|
||||||
GameList.SelectedItem = null;
|
|
||||||
RefreshVisibleGames();
|
|
||||||
StatusBarRight.Text = $"Removed “{game.Name}” from the library. Re-add its folder to restore it.";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshVisibleGames()
|
private void RefreshVisibleGames()
|
||||||
{
|
{
|
||||||
var query = SearchBox.Text?.Trim() ?? string.Empty;
|
var query = SearchBox.Text?.Trim() ?? string.Empty;
|
||||||
var selectedPath = (GameList.SelectedItem as GameEntry)?.Path;
|
var selected = GameList.SelectedItem as GameEntry;
|
||||||
|
|
||||||
_visibleGames.Clear();
|
_visibleGames.Clear();
|
||||||
foreach (var game in _allGames)
|
foreach (var game in _allGames)
|
||||||
{
|
{
|
||||||
if (query.Length == 0 ||
|
if (query.Length == 0 ||
|
||||||
game.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
game.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||||
game.Path.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
game.Path.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||||
(game.TitleId?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false))
|
|
||||||
{
|
{
|
||||||
_visibleGames.Add(game);
|
_visibleGames.Add(game);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GameCountText.Text = _visibleGames.Count == 1 ? "1 game" : $"{_visibleGames.Count} games";
|
GameCountText.Text = _visibleGames.Count.ToString();
|
||||||
|
if (selected is not null && _visibleGames.Contains(selected))
|
||||||
if (selectedPath is not null &&
|
|
||||||
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, StringComparison.OrdinalIgnoreCase))
|
|
||||||
is { } reselected)
|
|
||||||
{
|
{
|
||||||
GameList.SelectedItem = reselected;
|
GameList.SelectedItem = selected;
|
||||||
}
|
|
||||||
|
|
||||||
EmptyState.IsVisible = _visibleGames.Count == 0;
|
|
||||||
if (_visibleGames.Count == 0)
|
|
||||||
{
|
|
||||||
var hasFilter = query.Length > 0;
|
|
||||||
EmptyStateTitle.Text = hasFilter ? "No games match your search" : "Your library is empty";
|
|
||||||
EmptyStateHint.Text = hasFilter
|
|
||||||
? $"Nothing in the library matches “{query}”."
|
|
||||||
: "Add a folder containing your games to get started.";
|
|
||||||
EmptyAddFolderButton.IsVisible = !hasFilter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateSelectedGame();
|
UpdateSelectedGame();
|
||||||
@@ -793,115 +370,16 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
SelectedGameTitle.Text = game.Name;
|
SelectedGameTitle.Text = game.Name;
|
||||||
SelectedGamePath.Text = game.Path;
|
SelectedGamePath.Text = game.Path;
|
||||||
SelectedCoverPanel.DataContext = game;
|
|
||||||
_ = UpdateBackdropAsync(game);
|
|
||||||
PlaySelectedGamePreview(game);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SelectedGameTitle.Text = "No game selected";
|
SelectedGameTitle.Text = "No game selected";
|
||||||
SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly.";
|
SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly.";
|
||||||
SelectedCoverPanel.DataContext = null;
|
|
||||||
_ = UpdateBackdropAsync(null);
|
|
||||||
_sndPreview.Stop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateRunButtons();
|
UpdateRunButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loops the selected game's sce_sys/snd0.at9 preview music, console
|
|
||||||
/// home screen style. Silent while a game is running or when disabled
|
|
||||||
/// in the options.
|
|
||||||
/// </summary>
|
|
||||||
private void PlaySelectedGamePreview(GameEntry game)
|
|
||||||
{
|
|
||||||
if (_isRunning || !_settings.PlayTitleMusic)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var directory = Path.GetDirectoryName(game.Path);
|
|
||||||
var sndPath = directory is null ? null : Path.Combine(directory, "sce_sys", "snd0.at9");
|
|
||||||
if (sndPath is not null && File.Exists(sndPath))
|
|
||||||
{
|
|
||||||
_sndPreview.Play(sndPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_sndPreview.Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTitleMusicToggled()
|
|
||||||
{
|
|
||||||
_settings.PlayTitleMusic = TitleMusicToggle.IsChecked == true;
|
|
||||||
if (!_settings.PlayTitleMusic)
|
|
||||||
{
|
|
||||||
_sndPreview.Stop();
|
|
||||||
}
|
|
||||||
else if (GameList.SelectedItem is GameEntry game)
|
|
||||||
{
|
|
||||||
PlaySelectedGamePreview(game);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Pauses the preview music while the window is minimized.</summary>
|
|
||||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
|
||||||
{
|
|
||||||
base.OnPropertyChanged(change);
|
|
||||||
if (change.Property == WindowStateProperty)
|
|
||||||
{
|
|
||||||
if (WindowState == WindowState.Minimized)
|
|
||||||
{
|
|
||||||
_sndPreview.Pause();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_sndPreview.Resume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fades the window backdrop to the selected game's key art. The image
|
|
||||||
/// decodes off the UI thread and is cached on the entry; a newer
|
|
||||||
/// selection cancels the fade-in of an older one.
|
|
||||||
/// </summary>
|
|
||||||
private async Task UpdateBackdropAsync(GameEntry? game)
|
|
||||||
{
|
|
||||||
var generation = ++_backdropGeneration;
|
|
||||||
BackdropImage.Opacity = 0;
|
|
||||||
|
|
||||||
if (game?.BackgroundPath is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game.Background is null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var path = game.BackgroundPath;
|
|
||||||
game.Background = await Task.Run(() =>
|
|
||||||
{
|
|
||||||
using var stream = File.OpenRead(path);
|
|
||||||
return Bitmap.DecodeToWidth(stream, 1600);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return; // undecodable key art: keep the plain background
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (generation == _backdropGeneration)
|
|
||||||
{
|
|
||||||
BackdropImage.Source = game.Background;
|
|
||||||
BackdropImage.Opacity = 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Launching ----
|
// ---- Launching ----
|
||||||
|
|
||||||
private async Task OpenFileAsync()
|
private async Task OpenFileAsync()
|
||||||
@@ -928,11 +406,11 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
if (GameList.SelectedItem is GameEntry game)
|
if (GameList.SelectedItem is GameEntry game)
|
||||||
{
|
{
|
||||||
Launch(game.Path, game.Name, game.TitleId);
|
Launch(game.Path, game.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Launch(string ebootPath, string displayName, string? titleId = null)
|
private void Launch(string ebootPath, string displayName)
|
||||||
{
|
{
|
||||||
if (_isRunning)
|
if (_isRunning)
|
||||||
{
|
{
|
||||||
@@ -949,7 +427,6 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_sndPreview.Stop();
|
|
||||||
ReadControlsIntoSettings();
|
ReadControlsIntoSettings();
|
||||||
_settings.Save();
|
_settings.Save();
|
||||||
|
|
||||||
@@ -971,24 +448,6 @@ public partial class MainWindow : Window
|
|||||||
arguments.Add(ebootPath);
|
arguments.Add(ebootPath);
|
||||||
|
|
||||||
_consoleLines.Clear();
|
_consoleLines.Clear();
|
||||||
ConsoleToggle.IsChecked = true;
|
|
||||||
|
|
||||||
// Mirror everything the console pane shows into a log file for the
|
|
||||||
// duration of the run, regardless of the emulator's log level.
|
|
||||||
DropFileLog();
|
|
||||||
if (_settings.LogToFile && BuildLogFilePath(titleId) is { } logFilePath)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_fileLog = new StreamWriter(logFilePath, append: false);
|
|
||||||
AppendConsoleLine($"Log file: {logFilePath}", DimLineBrush);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
AppendConsoleLine($"Could not open the log file: {ex.Message}", WarningLineBrush);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
|
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
|
||||||
|
|
||||||
var emulator = new EmulatorProcess();
|
var emulator = new EmulatorProcess();
|
||||||
@@ -1003,7 +462,6 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
emulator.Dispose();
|
emulator.Dispose();
|
||||||
AppendConsoleLine($"Failed to start the emulator: {ex.Message}", ErrorLineBrush);
|
AppendConsoleLine($"Failed to start the emulator: {ex.Message}", ErrorLineBrush);
|
||||||
DropFileLog();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,37 +473,6 @@ public partial class MainWindow : Window
|
|||||||
UpdateRunButtons();
|
UpdateRunButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Builds "user/logs/<titleId>-<timestamp>.log" next to the emulator
|
|
||||||
/// executable, following the same portable-data convention as savedata.
|
|
||||||
/// </summary>
|
|
||||||
private string? BuildLogFilePath(string? titleId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var exeDirectory = Path.GetDirectoryName(_emulatorExePath);
|
|
||||||
if (string.IsNullOrEmpty(exeDirectory))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var logsDirectory = Path.Combine(exeDirectory, "user", "logs");
|
|
||||||
Directory.CreateDirectory(logsDirectory);
|
|
||||||
|
|
||||||
var id = string.IsNullOrWhiteSpace(titleId) ? "UNKNOWN" : titleId;
|
|
||||||
foreach (var invalid in Path.GetInvalidFileNameChars())
|
|
||||||
{
|
|
||||||
id = id.Replace(invalid, '_');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Path.Combine(logsDirectory, $"{id}-{DateTime.Now:yyyyMMdd-HHmmss}.log");
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return null; // unwritable location: launch continues without a log file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnEmulatorExited(int exitCode)
|
private void OnEmulatorExited(int exitCode)
|
||||||
{
|
{
|
||||||
FlushPendingConsoleLines();
|
FlushPendingConsoleLines();
|
||||||
@@ -1064,7 +491,6 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
var brush = exitCode == 0 ? SuccessLineBrush : ErrorLineBrush;
|
var brush = exitCode == 0 ? SuccessLineBrush : ErrorLineBrush;
|
||||||
AppendConsoleLine($"Process exited with code {exitCode} ({meaning}).", brush);
|
AppendConsoleLine($"Process exited with code {exitCode} ({meaning}).", brush);
|
||||||
CloseFileLogSoon();
|
|
||||||
|
|
||||||
StatusDot.Fill = exitCode == 0 ? (IBrush)SuccessLineBrush : ErrorLineBrush;
|
StatusDot.Fill = exitCode == 0 ? (IBrush)SuccessLineBrush : ErrorLineBrush;
|
||||||
StatusText.Text = $"Exited with code {exitCode} ({meaning})";
|
StatusText.Text = $"Exited with code {exitCode} ({meaning})";
|
||||||
@@ -1091,12 +517,9 @@ public partial class MainWindow : Window
|
|||||||
var incoming = new List<LogLine>();
|
var incoming = new List<LogLine>();
|
||||||
while (_pendingLines.TryDequeue(out var pending))
|
while (_pendingLines.TryDequeue(out var pending))
|
||||||
{
|
{
|
||||||
WriteFileLog(pending.Line);
|
|
||||||
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
|
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
|
||||||
}
|
}
|
||||||
|
|
||||||
FlushFileLog();
|
|
||||||
|
|
||||||
if (incoming.Count >= MaxConsoleLines)
|
if (incoming.Count >= MaxConsoleLines)
|
||||||
{
|
{
|
||||||
// A burst larger than the cap: keep only the newest lines.
|
// A burst larger than the cap: keep only the newest lines.
|
||||||
@@ -1125,81 +548,11 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private void AppendConsoleLine(string text, IBrush brush)
|
private void AppendConsoleLine(string text, IBrush brush)
|
||||||
{
|
{
|
||||||
WriteFileLog(text);
|
|
||||||
FlushFileLog();
|
|
||||||
_consoleLines.Add(new LogLine(text, brush));
|
_consoleLines.Add(new LogLine(text, brush));
|
||||||
_autoScrollTicks = 3;
|
_autoScrollTicks = 3;
|
||||||
MaybeAutoScroll();
|
MaybeAutoScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Console-to-file mirroring ----
|
|
||||||
|
|
||||||
private void WriteFileLog(string text)
|
|
||||||
{
|
|
||||||
if (_fileLog is not { } writer)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
writer.Write('[');
|
|
||||||
writer.Write(DateTime.Now.ToString("HH:mm:ss.fff"));
|
|
||||||
writer.Write("] ");
|
|
||||||
writer.WriteLine(text);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
DropFileLog(); // unwritable (disk full, etc.): stop mirroring
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FlushFileLog()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_fileLog?.Flush();
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
DropFileLog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DropFileLog()
|
|
||||||
{
|
|
||||||
var writer = _fileLog;
|
|
||||||
_fileLog = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
writer?.Dispose();
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The pipe reader threads can deliver a final burst after the exit
|
|
||||||
/// event, so the file stays open for one more flush cycle.
|
|
||||||
/// </summary>
|
|
||||||
private void CloseFileLogSoon()
|
|
||||||
{
|
|
||||||
if (_fileLog is not { } writer)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatcherTimer.RunOnce(() =>
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(_fileLog, writer))
|
|
||||||
{
|
|
||||||
FlushPendingConsoleLines();
|
|
||||||
DropFileLog();
|
|
||||||
}
|
|
||||||
}, TimeSpan.FromMilliseconds(400));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MaybeAutoScroll()
|
private void MaybeAutoScroll()
|
||||||
{
|
{
|
||||||
// ScrollToEnd is applied over a few flush-timer ticks because the
|
// ScrollToEnd is applied over a few flush-timer ticks because the
|
||||||
|
|||||||
@@ -12,12 +12,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Version>0.0.1</Version>
|
<Version>0.0.1</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Dependency-free; provides the BuildInfo provenance shown in the
|
|
||||||
title bar. -->
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" />
|
<PackageReference Include="Avalonia" />
|
||||||
<PackageReference Include="Avalonia.Desktop" />
|
<PackageReference Include="Avalonia.Desktop" />
|
||||||
@@ -27,18 +21,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<AvaloniaResource Include="..\..\assets\images\logo.png" Link="Assets/logo.png" />
|
||||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
|
||||||
with the emulator's pad HLE. They are dependency-free, so they are
|
|
||||||
compiled in directly rather than pulling a reference to all of
|
|
||||||
SharpEmu.Libs into the launcher. -->
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="..\SharpEmu.Libs\Pad\PadState.cs" Link="Input/PadState.cs" />
|
|
||||||
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
|
|
||||||
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
|
|
||||||
<Compile Include="..\SharpEmu.Libs\Pad\XInputReader.cs" Link="Input/XInputReader.cs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,301 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using LibAtrac9;
|
|
||||||
|
|
||||||
namespace SharpEmu.GUI;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loops a game's sce_sys/snd0.at9 preview music while the game is selected
|
|
||||||
/// in the library, like the console home screen. The ATRAC9 stream is decoded
|
|
||||||
/// to WAV on a background task (vendored LibAtrac9); playback uses winmm's
|
|
||||||
/// PlaySound, so this is Windows-only and a no-op elsewhere.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class SndPreviewPlayer
|
|
||||||
{
|
|
||||||
private const uint SND_ASYNC = 0x0001;
|
|
||||||
private const uint SND_NODEFAULT = 0x0002;
|
|
||||||
private const uint SND_MEMORY = 0x0004;
|
|
||||||
private const uint SND_LOOP = 0x0008;
|
|
||||||
|
|
||||||
private readonly object _sync = new();
|
|
||||||
private int _generation;
|
|
||||||
private GCHandle _pinnedWav;
|
|
||||||
private bool _playing;
|
|
||||||
private bool _paused;
|
|
||||||
private string? _cachedPath;
|
|
||||||
private byte[]? _cachedWav;
|
|
||||||
|
|
||||||
/// <summary>Starts looping the given snd0.at9 after a short debounce.</summary>
|
|
||||||
public void Play(string at9Path)
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int generation;
|
|
||||||
lock (_sync)
|
|
||||||
{
|
|
||||||
generation = ++_generation;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
// Debounce so skimming through the library does not decode (or
|
|
||||||
// start) a preview per tile.
|
|
||||||
await Task.Delay(300).ConfigureAwait(false);
|
|
||||||
|
|
||||||
byte[]? wav;
|
|
||||||
lock (_sync)
|
|
||||||
{
|
|
||||||
if (generation != _generation)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
wav = string.Equals(_cachedPath, at9Path, StringComparison.OrdinalIgnoreCase)
|
|
||||||
? _cachedWav
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wav is null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
wav = DecodeAt9ToWav(File.ReadAllBytes(at9Path));
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return; // corrupt or unsupported preview: stay silent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_sync)
|
|
||||||
{
|
|
||||||
if (generation != _generation)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_cachedPath = at9Path;
|
|
||||||
_cachedWav = wav;
|
|
||||||
StopLocked();
|
|
||||||
|
|
||||||
// The WAV image must stay pinned while winmm plays from it.
|
|
||||||
_pinnedWav = GCHandle.Alloc(wav, GCHandleType.Pinned);
|
|
||||||
_playing = PlaySound(_pinnedWav.AddrOfPinnedObject(), 0, SND_MEMORY | SND_ASYNC | SND_LOOP | SND_NODEFAULT);
|
|
||||||
if (!_playing)
|
|
||||||
{
|
|
||||||
_pinnedWav.Free();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_sync)
|
|
||||||
{
|
|
||||||
_generation++;
|
|
||||||
StopLocked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Silences playback but keeps the decoded track ready, so
|
|
||||||
/// <see cref="Resume"/> can restart it (winmm cannot truly pause).
|
|
||||||
/// </summary>
|
|
||||||
public void Pause()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_sync)
|
|
||||||
{
|
|
||||||
if (!_playing)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = PlaySound(0, 0, 0);
|
|
||||||
_playing = false;
|
|
||||||
_paused = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Restarts the track silenced by <see cref="Pause"/>.</summary>
|
|
||||||
public void Resume()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_sync)
|
|
||||||
{
|
|
||||||
if (!_paused || !_pinnedWav.IsAllocated)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_paused = false;
|
|
||||||
_playing = PlaySound(_pinnedWav.AddrOfPinnedObject(), 0, SND_MEMORY | SND_ASYNC | SND_LOOP | SND_NODEFAULT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StopLocked()
|
|
||||||
{
|
|
||||||
_ = PlaySound(0, 0, 0);
|
|
||||||
_playing = false;
|
|
||||||
_paused = false;
|
|
||||||
if (_pinnedWav.IsAllocated)
|
|
||||||
{
|
|
||||||
_pinnedWav.Free();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly Guid Atrac9SubFormat = new("47E142D2-36BA-4D8D-88FC-61654F8C836C");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decodes a Sony AT9 (RIFF-wrapped ATRAC9) file to a PCM16 WAV image.
|
|
||||||
/// Layout per Sony's container: an extensible fmt chunk whose extension
|
|
||||||
/// carries the 4-byte codec config, a fact chunk with the sample count
|
|
||||||
/// and encoder delay, and superframes in the data chunk.
|
|
||||||
/// </summary>
|
|
||||||
private static byte[] DecodeAt9ToWav(byte[] file)
|
|
||||||
{
|
|
||||||
using var reader = new BinaryReader(new MemoryStream(file), Encoding.ASCII);
|
|
||||||
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF")
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Not a RIFF file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.BaseStream.Position += 4; // riff size
|
|
||||||
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE")
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Not a WAVE file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[]? configData = null;
|
|
||||||
var sampleCount = 0;
|
|
||||||
var encoderDelay = 0;
|
|
||||||
var dataOffset = -1;
|
|
||||||
var dataSize = 0;
|
|
||||||
|
|
||||||
while (reader.BaseStream.Position + 8 <= reader.BaseStream.Length)
|
|
||||||
{
|
|
||||||
var chunkId = Encoding.ASCII.GetString(reader.ReadBytes(4));
|
|
||||||
var chunkSize = reader.ReadInt32();
|
|
||||||
var chunkStart = reader.BaseStream.Position;
|
|
||||||
switch (chunkId)
|
|
||||||
{
|
|
||||||
case "fmt ":
|
|
||||||
var formatTag = reader.ReadUInt16();
|
|
||||||
reader.BaseStream.Position = chunkStart + 24; // to SubFormat GUID
|
|
||||||
var subFormat = new Guid(reader.ReadBytes(16));
|
|
||||||
if (formatTag != 0xFFFE || subFormat != Atrac9SubFormat)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Not an ATRAC9 stream.");
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.BaseStream.Position += 4; // version info
|
|
||||||
configData = reader.ReadBytes(4);
|
|
||||||
break;
|
|
||||||
case "fact":
|
|
||||||
sampleCount = reader.ReadInt32();
|
|
||||||
reader.BaseStream.Position += 4; // input overlap delay
|
|
||||||
encoderDelay = reader.ReadInt32();
|
|
||||||
break;
|
|
||||||
case "data":
|
|
||||||
dataOffset = (int)chunkStart;
|
|
||||||
dataSize = chunkSize;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.BaseStream.Position = chunkStart + chunkSize + (chunkSize & 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configData is null || sampleCount <= 0 || dataOffset < 0)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException("Missing fmt, fact, or data chunk.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var decoder = new Atrac9Decoder();
|
|
||||||
decoder.Initialize(configData);
|
|
||||||
var config = decoder.Config;
|
|
||||||
|
|
||||||
var superframeCount = (sampleCount + encoderDelay + config.SuperframeSamples - 1) / config.SuperframeSamples;
|
|
||||||
superframeCount = Math.Min(superframeCount, dataSize / config.SuperframeBytes);
|
|
||||||
|
|
||||||
var channels = config.ChannelCount;
|
|
||||||
var pcmBuffer = new short[channels][];
|
|
||||||
for (var i = 0; i < channels; i++)
|
|
||||||
{
|
|
||||||
pcmBuffer[i] = new short[config.SuperframeSamples];
|
|
||||||
}
|
|
||||||
|
|
||||||
var wav = new byte[44 + (sampleCount * channels * 2)];
|
|
||||||
WriteWavHeader(wav, channels, config.SampleRate, sampleCount);
|
|
||||||
|
|
||||||
var superframe = new byte[config.SuperframeBytes];
|
|
||||||
var decodedIndex = 0L; // per-channel, includes the encoder delay
|
|
||||||
var written = 0;
|
|
||||||
for (var f = 0; f < superframeCount && written < sampleCount; f++)
|
|
||||||
{
|
|
||||||
Buffer.BlockCopy(file, dataOffset + (f * config.SuperframeBytes), superframe, 0, config.SuperframeBytes);
|
|
||||||
decoder.Decode(superframe, pcmBuffer);
|
|
||||||
for (var s = 0; s < config.SuperframeSamples && written < sampleCount; s++)
|
|
||||||
{
|
|
||||||
if (decodedIndex++ < encoderDelay)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sampleOffset = 44 + ((long)written * channels * 2);
|
|
||||||
for (var ch = 0; ch < channels; ch++)
|
|
||||||
{
|
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(
|
|
||||||
wav.AsSpan((int)(sampleOffset + (ch * 2))),
|
|
||||||
pcmBuffer[ch][s]);
|
|
||||||
}
|
|
||||||
|
|
||||||
written++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return wav;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteWavHeader(byte[] wav, int channels, int sampleRate, int sampleCount)
|
|
||||||
{
|
|
||||||
var span = wav.AsSpan();
|
|
||||||
Encoding.ASCII.GetBytes("RIFF").CopyTo(span);
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(span[4..], wav.Length - 8);
|
|
||||||
Encoding.ASCII.GetBytes("WAVE").CopyTo(span[8..]);
|
|
||||||
Encoding.ASCII.GetBytes("fmt ").CopyTo(span[12..]);
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(span[16..], 16);
|
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(span[20..], 1); // PCM
|
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(span[22..], (short)channels);
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(span[24..], sampleRate);
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(span[28..], sampleRate * channels * 2);
|
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(span[32..], (short)(channels * 2));
|
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(span[34..], 16); // bits per sample
|
|
||||||
Encoding.ASCII.GetBytes("data").CopyTo(span[36..]);
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(span[40..], sampleCount * channels * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport("winmm.dll")]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool PlaySound(nint sound, nint module, uint flags);
|
|
||||||
}
|
|
||||||
@@ -180,9 +180,6 @@
|
|||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.88.9",
|
"resolved": "2.88.9",
|
||||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||||
},
|
|
||||||
"sharpemu.logging": {
|
|
||||||
"type": "Project"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,14 @@ public static class KernelExports
|
|||||||
ExportName = "exit",
|
ExportName = "exit",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
|
public static int Exit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
|
||||||
|
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "XKRegsFpEpk",
|
Nid = "XKRegsFpEpk",
|
||||||
@@ -165,7 +172,11 @@ public static class KernelExports
|
|||||||
ExportName = "_exit",
|
ExportName = "_exit",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int UnderscoreExit(CpuContext ctx) => RequestProcessExit(ctx, "_exit");
|
public static int UnderscoreExit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Ac86z8q7T8A",
|
Nid = "Ac86z8q7T8A",
|
||||||
@@ -426,13 +437,4 @@ public static class KernelExports
|
|||||||
{
|
{
|
||||||
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int RequestProcessExit(CpuContext ctx, string syscallName)
|
|
||||||
{
|
|
||||||
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] {syscallName}(status={status})");
|
|
||||||
GuestThreadExecution.RequestCurrentEntryExit(syscallName, status);
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,13 +67,6 @@ public static class KernelMemoryCompatExports
|
|||||||
private const int Einval = 22;
|
private const int Einval = 22;
|
||||||
private const int Erange = 34;
|
private const int Erange = 34;
|
||||||
private const int Struncate = 80;
|
private const int Struncate = 80;
|
||||||
private const int ClockRealtime = 0;
|
|
||||||
private const int ClockVirtual = 1;
|
|
||||||
private const int ClockProf = 2;
|
|
||||||
private const int ClockMonotonic = 4;
|
|
||||||
private const int ClockUptime = 5;
|
|
||||||
private const int ClockRealtimeFast = 10;
|
|
||||||
private const int ClockMonotonicFast = 12;
|
|
||||||
private const nuint DefaultLibcHeapAlignment = 16;
|
private const nuint DefaultLibcHeapAlignment = 16;
|
||||||
private const ushort KernelStatModeDirectory = 0x41FF;
|
private const ushort KernelStatModeDirectory = 0x41FF;
|
||||||
private const ushort KernelStatModeRegular = 0x81FF;
|
private const ushort KernelStatModeRegular = 0x81FF;
|
||||||
@@ -2000,55 +1993,23 @@ public static class KernelMemoryCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int ClockGettime(CpuContext ctx)
|
public static int ClockGettime(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var clockId = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var timespecAddress = ctx[CpuRegister.Rsi];
|
var timespecAddress = ctx[CpuRegister.Rsi];
|
||||||
if (timespecAddress == 0)
|
if (timespecAddress == 0)
|
||||||
{
|
{
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
long seconds;
|
var now = DateTimeOffset.UtcNow;
|
||||||
long nanoseconds;
|
var seconds = now.ToUnixTimeSeconds();
|
||||||
switch (clockId)
|
var nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
|
||||||
|
if (!ctx.TryWriteUInt64(timespecAddress, unchecked((ulong)seconds)) ||
|
||||||
|
!ctx.TryWriteUInt64(timespecAddress + sizeof(long), unchecked((ulong)nanoseconds)))
|
||||||
{
|
{
|
||||||
case ClockRealtime:
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
case ClockRealtimeFast:
|
|
||||||
case ClockVirtual:
|
|
||||||
case ClockProf:
|
|
||||||
{
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
|
||||||
seconds = now.ToUnixTimeSeconds();
|
|
||||||
nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case ClockMonotonic:
|
|
||||||
case ClockMonotonicFast:
|
|
||||||
case ClockUptime:
|
|
||||||
KernelRuntimeCompatExports.GetProcessMonotonicTime(out seconds, out nanoseconds);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Span<byte> timespecBuffer = stackalloc byte[16];
|
|
||||||
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer, seconds);
|
|
||||||
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer[sizeof(long)..], nanoseconds);
|
|
||||||
|
|
||||||
if (!ctx.Memory.TryWrite(timespecAddress, timespecBuffer))
|
|
||||||
{
|
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return 0;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -3885,59 +3846,33 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
private struct RegisterPrintfArgumentSource : IPrintfArgumentSource
|
private struct RegisterPrintfArgumentSource : IPrintfArgumentSource
|
||||||
{
|
{
|
||||||
// SysV AMD64: variadic integer/pointer args use the GP registers (rdi..r9, up to
|
|
||||||
// 6) and variadic float/double args use xmm0..xmm7 (8) — INDEPENDENT counters.
|
|
||||||
// Args beyond the registers spill to the stack in source order, so GP-overflow
|
|
||||||
// and FP-overflow share one stack cursor.
|
|
||||||
private readonly CpuContext _ctx;
|
private readonly CpuContext _ctx;
|
||||||
private int _gpIndex;
|
private int _gpIndex;
|
||||||
private int _fpIndex;
|
|
||||||
private int _stackIndex;
|
|
||||||
|
|
||||||
public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex)
|
public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex)
|
||||||
{
|
{
|
||||||
_ctx = ctx;
|
_ctx = ctx;
|
||||||
_gpIndex = gpIndex;
|
_gpIndex = gpIndex;
|
||||||
_fpIndex = 0;
|
|
||||||
_stackIndex = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ulong NextGpArg()
|
public ulong NextGpArg()
|
||||||
{
|
{
|
||||||
var index = _gpIndex;
|
var index = _gpIndex++;
|
||||||
if (index < 6)
|
return index switch
|
||||||
{
|
{
|
||||||
_gpIndex++;
|
0 => _ctx[CpuRegister.Rdi],
|
||||||
return index switch
|
1 => _ctx[CpuRegister.Rsi],
|
||||||
{
|
2 => _ctx[CpuRegister.Rdx],
|
||||||
0 => _ctx[CpuRegister.Rdi],
|
3 => _ctx[CpuRegister.Rcx],
|
||||||
1 => _ctx[CpuRegister.Rsi],
|
4 => _ctx[CpuRegister.R8],
|
||||||
2 => _ctx[CpuRegister.Rdx],
|
5 => _ctx[CpuRegister.R9],
|
||||||
3 => _ctx[CpuRegister.Rcx],
|
_ => ReadStackArg(_ctx, (ulong)(index - 6) * 8)
|
||||||
4 => _ctx[CpuRegister.R8],
|
};
|
||||||
_ => _ctx[CpuRegister.R9],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public double NextFloatArg()
|
public double NextFloatArg()
|
||||||
{
|
{
|
||||||
// Float/double variadic args live in xmm0..xmm7 (low 64 bits), NOT the GP
|
return BitConverter.Int64BitsToDouble(unchecked((long)NextGpArg()));
|
||||||
// registers. Reading them from GP prints garbage and desynchronizes every
|
|
||||||
// subsequent argument.
|
|
||||||
ulong bits;
|
|
||||||
if (_fpIndex < 8)
|
|
||||||
{
|
|
||||||
_ctx.GetXmmRegister(_fpIndex++, out bits, out _);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bits = ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
return BitConverter.Int64BitsToDouble(unchecked((long)bits));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,14 +194,13 @@ public static class KernelRuntimeCompatExports
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
GetProcessMonotonicTime(out seconds, out nanoseconds);
|
var elapsedTicks = Stopwatch.GetTimestamp() - _processStartCounter;
|
||||||
|
seconds = elapsedTicks / Stopwatch.Frequency;
|
||||||
|
nanoseconds = (elapsedTicks % Stopwatch.Frequency) * 1_000_000_000L / Stopwatch.Frequency;
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> timespecBuffer = stackalloc byte[16];
|
if (!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
|
||||||
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer, seconds);
|
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)nanoseconds)))
|
||||||
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer[sizeof(long)..], nanoseconds);
|
|
||||||
|
|
||||||
if (!ctx.Memory.TryWrite(timeAddress, timespecBuffer))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -618,13 +617,6 @@ public static class KernelRuntimeCompatExports
|
|||||||
return address != 0 && ctx.TryWriteInt32(address, value);
|
return address != 0 && ctx.TryWriteInt32(address, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void GetProcessMonotonicTime(out long seconds, out long nanoseconds)
|
|
||||||
{
|
|
||||||
var elapsedTicks = Stopwatch.GetTimestamp() - _processStartCounter;
|
|
||||||
seconds = elapsedTicks / Stopwatch.Frequency;
|
|
||||||
nanoseconds = (elapsedTicks % Stopwatch.Frequency) * 1_000_000_000L / Stopwatch.Frequency;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "bnZxYgAFeA0",
|
Nid = "bnZxYgAFeA0",
|
||||||
ExportName = "sceKernelGetSanitizerNewReplaceExternal",
|
ExportName = "sceKernelGetSanitizerNewReplaceExternal",
|
||||||
@@ -1477,42 +1469,26 @@ public static class KernelRuntimeCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int pageSize = 4096;
|
var bytes = new List<byte>(Math.Min(maxLength, 64));
|
||||||
const int inlineChunkSize = 64;
|
Span<byte> one = stackalloc byte[1];
|
||||||
Span<byte> buffer = stackalloc byte[Math.Min(maxLength, 512)];
|
for (var i = 0; i < maxLength; i++)
|
||||||
var length = 0;
|
|
||||||
|
|
||||||
for (var offset = 0; offset < maxLength && length < buffer.Length;)
|
|
||||||
{
|
{
|
||||||
var current = address + (ulong)offset;
|
if (!ctx.Memory.TryRead(address + (ulong)i, one))
|
||||||
var pageRemaining = pageSize - (int)(current & (pageSize - 1));
|
|
||||||
var chunkSize = Math.Min(
|
|
||||||
buffer.Length - length,
|
|
||||||
Math.Min(maxLength - offset, Math.Min(inlineChunkSize, pageRemaining)));
|
|
||||||
|
|
||||||
if (chunkSize <= 0)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunk = buffer.Slice(length, chunkSize);
|
if (one[0] == 0)
|
||||||
if (!ctx.Memory.TryRead(current, chunk))
|
|
||||||
{
|
{
|
||||||
return false;
|
value = Encoding.UTF8.GetString(bytes.ToArray());
|
||||||
}
|
|
||||||
|
|
||||||
var nulIndex = chunk.IndexOf((byte)0);
|
|
||||||
if (nulIndex >= 0)
|
|
||||||
{
|
|
||||||
value = Encoding.UTF8.GetString(buffer[..(length + nulIndex)]);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
length += chunkSize;
|
bytes.Add(one[0]);
|
||||||
offset += chunkSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
value = Encoding.UTF8.GetString(bytes.ToArray());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
|
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
|
||||||
@@ -1721,7 +1697,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReadOnlySpan<byte> stub = stackalloc byte[]
|
Span<byte> stub = stackalloc byte[]
|
||||||
{
|
{
|
||||||
0x0F, 0x31,
|
0x0F, 0x31,
|
||||||
0x48, 0xC1, 0xE2, 0x20,
|
0x48, 0xC1, 0xE2, 0x20,
|
||||||
@@ -1729,14 +1705,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
0xC3,
|
0xC3,
|
||||||
};
|
};
|
||||||
|
|
||||||
unsafe
|
Marshal.Copy(stub.ToArray(), 0, stubAddress, stub.Length);
|
||||||
{
|
|
||||||
fixed (byte* src = stub)
|
|
||||||
{
|
|
||||||
Buffer.MemoryCopy(src, (void*)stubAddress, stub.Length, stub.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Marshal.GetDelegateForFunctionPointer<RdtscDelegate>(stubAddress);
|
return Marshal.GetDelegateForFunctionPointer<RdtscDelegate>(stubAddress);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -1,437 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using Microsoft.Win32.SafeHandles;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads a DualSense controller over raw HID on a background thread.
|
|
||||||
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
|
|
||||||
/// activated by requesting feature report 0x05), with hot-plug retry.
|
|
||||||
/// </summary>
|
|
||||||
internal static class DualSenseReader
|
|
||||||
{
|
|
||||||
private const ushort SonyVendorId = 0x054C;
|
|
||||||
private const ushort DualSenseProductId = 0x0CE6;
|
|
||||||
private const ushort DualSenseEdgeProductId = 0x0DF2;
|
|
||||||
|
|
||||||
private static readonly object Gate = new();
|
|
||||||
private static PadState _state;
|
|
||||||
private static bool _started;
|
|
||||||
|
|
||||||
// Output (rumble/lightbar) state, all guarded by Gate.
|
|
||||||
private static string? _devicePath;
|
|
||||||
private static bool _bluetooth;
|
|
||||||
private static bool _outputReady;
|
|
||||||
private static bool _lightbarSetupPending;
|
|
||||||
private static byte _outputSequence;
|
|
||||||
private static FileStream? _outputStream;
|
|
||||||
private static byte _motorLeft;
|
|
||||||
private static byte _motorRight;
|
|
||||||
private static byte _lightbarRed;
|
|
||||||
private static byte _lightbarGreen;
|
|
||||||
private static byte _lightbarBlue = 64; // PS-style blue default
|
|
||||||
private static byte _playerLeds = 0x04; // center LED = player 1
|
|
||||||
|
|
||||||
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
|
||||||
internal static void EnsureStarted()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
if (_started)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_started = true;
|
|
||||||
var thread = new Thread(ReadLoop)
|
|
||||||
{
|
|
||||||
IsBackground = true,
|
|
||||||
Name = "DualSenseReader",
|
|
||||||
};
|
|
||||||
thread.Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool TryGetState(out PadState state)
|
|
||||||
{
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
state = _state;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.Connected;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SetState(in PadState state)
|
|
||||||
{
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
_state = state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Sets rumble; large = left/strong motor, small = right/weak.</summary>
|
|
||||||
internal static void SetRumble(byte largeMotor, byte smallMotor)
|
|
||||||
{
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
if (_motorLeft == largeMotor && _motorRight == smallMotor)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_motorLeft = largeMotor;
|
|
||||||
_motorRight = smallMotor;
|
|
||||||
SendOutputLocked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void SetLightbar(byte red, byte green, byte blue)
|
|
||||||
{
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
if (_lightbarRed == red && _lightbarGreen == green && _lightbarBlue == blue)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_lightbarRed = red;
|
|
||||||
_lightbarGreen = green;
|
|
||||||
_lightbarBlue = blue;
|
|
||||||
SendOutputLocked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void ResetLightbar() => SetLightbar(0, 0, 64);
|
|
||||||
|
|
||||||
private static void OnDeviceIdentified(string path, bool bluetooth)
|
|
||||||
{
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
_devicePath = path;
|
|
||||||
_bluetooth = bluetooth;
|
|
||||||
_outputReady = true;
|
|
||||||
_lightbarSetupPending = true;
|
|
||||||
// Announce ourselves on the hardware: default lightbar + player 1 LED.
|
|
||||||
SendOutputLocked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OnDeviceLost()
|
|
||||||
{
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
_devicePath = null;
|
|
||||||
_outputReady = false;
|
|
||||||
_motorLeft = 0;
|
|
||||||
_motorRight = 0;
|
|
||||||
_outputStream?.Dispose();
|
|
||||||
_outputStream = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SendOutputLocked()
|
|
||||||
{
|
|
||||||
if (!_outputReady || _devicePath is null)
|
|
||||||
{
|
|
||||||
return; // flushed by OnDeviceIdentified once connected
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_outputStream is null)
|
|
||||||
{
|
|
||||||
var handle = HidNative.CreateFile(
|
|
||||||
_devicePath,
|
|
||||||
HidNative.GenericRead | HidNative.GenericWrite,
|
|
||||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
|
||||||
0, HidNative.OpenExisting, 0, 0);
|
|
||||||
if (handle.IsInvalid)
|
|
||||||
{
|
|
||||||
handle.Dispose();
|
|
||||||
return; // read-only device access: outputs unavailable
|
|
||||||
}
|
|
||||||
|
|
||||||
_outputStream = new FileStream(handle, FileAccess.Write, bufferSize: 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
var report = BuildOutputReportLocked();
|
|
||||||
_outputStream.Write(report, 0, report.Length);
|
|
||||||
_outputStream.Flush();
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
_outputStream?.Dispose();
|
|
||||||
_outputStream = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] BuildOutputReportLocked()
|
|
||||||
{
|
|
||||||
// Common 47-byte output payload (offsets per the DualSense output
|
|
||||||
// report layout, same as Linux hid-playstation).
|
|
||||||
Span<byte> common = stackalloc byte[47];
|
|
||||||
common[0] = 0x03; // valid_flag0: compatible vibration + haptics select
|
|
||||||
common[1] = 0x04 | 0x10; // valid_flag1: lightbar + player indicator
|
|
||||||
common[2] = _motorRight; // right (weak) motor
|
|
||||||
common[3] = _motorLeft; // left (strong) motor
|
|
||||||
if (_lightbarSetupPending)
|
|
||||||
{
|
|
||||||
common[38] |= 0x02; // valid_flag2: lightbar setup control enable
|
|
||||||
common[41] = 0x01; // lightbar_setup: light on
|
|
||||||
_lightbarSetupPending = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
common[43] = _playerLeds;
|
|
||||||
common[44] = _lightbarRed;
|
|
||||||
common[45] = _lightbarGreen;
|
|
||||||
common[46] = _lightbarBlue;
|
|
||||||
|
|
||||||
if (!_bluetooth)
|
|
||||||
{
|
|
||||||
var usbReport = new byte[48];
|
|
||||||
usbReport[0] = 0x02;
|
|
||||||
common.CopyTo(usbReport.AsSpan(1));
|
|
||||||
return usbReport;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bluetooth: 0x31 wrapper with sequence tag and CRC32 over a 0xA2
|
|
||||||
// seed byte plus the first 74 report bytes.
|
|
||||||
var btReport = new byte[78];
|
|
||||||
btReport[0] = 0x31;
|
|
||||||
btReport[1] = (byte)((_outputSequence & 0x0F) << 4);
|
|
||||||
_outputSequence = (byte)((_outputSequence + 1) & 0x0F);
|
|
||||||
btReport[2] = 0x10;
|
|
||||||
common.CopyTo(btReport.AsSpan(3));
|
|
||||||
var crc = Crc32(0xA2, btReport.AsSpan(0, 74));
|
|
||||||
btReport[74] = (byte)crc;
|
|
||||||
btReport[75] = (byte)(crc >> 8);
|
|
||||||
btReport[76] = (byte)(crc >> 16);
|
|
||||||
btReport[77] = (byte)(crc >> 24);
|
|
||||||
return btReport;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static uint Crc32(byte seed, ReadOnlySpan<byte> data)
|
|
||||||
{
|
|
||||||
var crc = Crc32Update(0xFFFFFFFFu, seed);
|
|
||||||
foreach (var value in data)
|
|
||||||
{
|
|
||||||
crc = Crc32Update(crc, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ~crc;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static uint Crc32Update(uint crc, byte value)
|
|
||||||
{
|
|
||||||
crc ^= value;
|
|
||||||
for (var bit = 0; bit < 8; bit++)
|
|
||||||
{
|
|
||||||
crc = (crc >> 1) ^ (0xEDB88320u & (uint)-(int)(crc & 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
return crc;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadLoop()
|
|
||||||
{
|
|
||||||
var announcedConnect = false;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
SafeFileHandle? handle = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
handle = OpenDualSense(out var devicePath);
|
|
||||||
if (handle is null || devicePath is null)
|
|
||||||
{
|
|
||||||
SetState(default);
|
|
||||||
announcedConnect = false;
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bluetooth quirk: the DualSense sends a simplified report
|
|
||||||
// until feature report 0x05 is requested, which switches it
|
|
||||||
// to the full 0x31 input report. Harmless over USB.
|
|
||||||
var feature = new byte[41];
|
|
||||||
feature[0] = 0x05;
|
|
||||||
_ = HidNative.HidD_GetFeature(handle, feature, feature.Length);
|
|
||||||
|
|
||||||
if (!announcedConnect)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] DualSense controller connected.");
|
|
||||||
announcedConnect = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var stream = new FileStream(handle, FileAccess.Read, bufferSize: 1);
|
|
||||||
handle = null; // stream owns it now
|
|
||||||
var buffer = new byte[256];
|
|
||||||
var transportKnown = false;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var read = stream.Read(buffer, 0, buffer.Length);
|
|
||||||
if (read <= 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TryParseReport(buffer.AsSpan(0, read), out var state))
|
|
||||||
{
|
|
||||||
if (!transportKnown)
|
|
||||||
{
|
|
||||||
// The first parsed report tells us the transport,
|
|
||||||
// which the output (rumble/lightbar) path needs.
|
|
||||||
transportKnown = true;
|
|
||||||
OnDeviceIdentified(devicePath, bluetooth: buffer[0] == 0x31);
|
|
||||||
}
|
|
||||||
|
|
||||||
SetState(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// Unplugged or read error: fall through and retry.
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
handle?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (announcedConnect)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] DualSense controller disconnected.");
|
|
||||||
announcedConnect = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
OnDeviceLost();
|
|
||||||
SetState(default);
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SafeFileHandle? OpenDualSense(out string? devicePath)
|
|
||||||
{
|
|
||||||
devicePath = null;
|
|
||||||
foreach (var path in HidNative.EnumerateHidDevicePaths())
|
|
||||||
{
|
|
||||||
// Open without access rights just to query VID/PID.
|
|
||||||
using var probe = HidNative.CreateFile(
|
|
||||||
path, 0, HidNative.FileShareRead | HidNative.FileShareWrite, 0, HidNative.OpenExisting, 0, 0);
|
|
||||||
if (probe.IsInvalid)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var attributes = new HidNative.HiddAttributes { Size = 12 };
|
|
||||||
if (!HidNative.HidD_GetAttributes(probe, ref attributes) ||
|
|
||||||
attributes.VendorId != SonyVendorId ||
|
|
||||||
(attributes.ProductId != DualSenseProductId && attributes.ProductId != DualSenseEdgeProductId))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read+write so feature reports work; fall back to read-only.
|
|
||||||
var handle = HidNative.CreateFile(
|
|
||||||
path,
|
|
||||||
HidNative.GenericRead | HidNative.GenericWrite,
|
|
||||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
|
||||||
0, HidNative.OpenExisting, 0, 0);
|
|
||||||
if (handle.IsInvalid)
|
|
||||||
{
|
|
||||||
handle.Dispose();
|
|
||||||
handle = HidNative.CreateFile(
|
|
||||||
path,
|
|
||||||
HidNative.GenericRead,
|
|
||||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
|
||||||
0, HidNative.OpenExisting, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!handle.IsInvalid)
|
|
||||||
{
|
|
||||||
devicePath = path;
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
handle.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryParseReport(ReadOnlySpan<byte> report, out PadState state)
|
|
||||||
{
|
|
||||||
// USB: report id 0x01, payload starts at [1].
|
|
||||||
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
|
|
||||||
int offset;
|
|
||||||
if (report.Length >= 11 && report[0] == 0x01)
|
|
||||||
{
|
|
||||||
offset = 1;
|
|
||||||
}
|
|
||||||
else if (report.Length >= 12 && report[0] == 0x31)
|
|
||||||
{
|
|
||||||
offset = 2;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
state = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var leftX = report[offset + 0];
|
|
||||||
var leftY = report[offset + 1];
|
|
||||||
var rightX = report[offset + 2];
|
|
||||||
var rightY = report[offset + 3];
|
|
||||||
var l2 = report[offset + 4];
|
|
||||||
var r2 = report[offset + 5];
|
|
||||||
var buttons0 = report[offset + 7];
|
|
||||||
var buttons1 = report[offset + 8];
|
|
||||||
var buttons2 = report[offset + 9];
|
|
||||||
|
|
||||||
uint buttons = 0;
|
|
||||||
buttons |= (buttons0 & 0x10) != 0 ? OrbisPadButton.Square : 0;
|
|
||||||
buttons |= (buttons0 & 0x20) != 0 ? OrbisPadButton.Cross : 0;
|
|
||||||
buttons |= (buttons0 & 0x40) != 0 ? OrbisPadButton.Circle : 0;
|
|
||||||
buttons |= (buttons0 & 0x80) != 0 ? OrbisPadButton.Triangle : 0;
|
|
||||||
buttons |= HatToButtons(buttons0 & 0x0F);
|
|
||||||
buttons |= (buttons1 & 0x01) != 0 ? OrbisPadButton.L1 : 0;
|
|
||||||
buttons |= (buttons1 & 0x02) != 0 ? OrbisPadButton.R1 : 0;
|
|
||||||
buttons |= (buttons1 & 0x04) != 0 ? OrbisPadButton.L2 : 0;
|
|
||||||
buttons |= (buttons1 & 0x08) != 0 ? OrbisPadButton.R2 : 0;
|
|
||||||
buttons |= (buttons1 & 0x20) != 0 ? OrbisPadButton.Options : 0;
|
|
||||||
buttons |= (buttons1 & 0x40) != 0 ? OrbisPadButton.L3 : 0;
|
|
||||||
buttons |= (buttons1 & 0x80) != 0 ? OrbisPadButton.R3 : 0;
|
|
||||||
buttons |= (buttons2 & 0x02) != 0 ? OrbisPadButton.TouchPad : 0;
|
|
||||||
|
|
||||||
state = new PadState(
|
|
||||||
Connected: true,
|
|
||||||
Buttons: buttons,
|
|
||||||
LeftX: leftX,
|
|
||||||
LeftY: leftY,
|
|
||||||
RightX: rightX,
|
|
||||||
RightY: rightY,
|
|
||||||
L2: l2,
|
|
||||||
R2: r2);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static uint HatToButtons(int hat) => hat switch
|
|
||||||
{
|
|
||||||
0 => OrbisPadButton.Up,
|
|
||||||
1 => OrbisPadButton.Up | OrbisPadButton.Right,
|
|
||||||
2 => OrbisPadButton.Right,
|
|
||||||
3 => OrbisPadButton.Right | OrbisPadButton.Down,
|
|
||||||
4 => OrbisPadButton.Down,
|
|
||||||
5 => OrbisPadButton.Down | OrbisPadButton.Left,
|
|
||||||
6 => OrbisPadButton.Left,
|
|
||||||
7 => OrbisPadButton.Left | OrbisPadButton.Up,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using Microsoft.Win32.SafeHandles;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Minimal Win32 HID interop used to talk to a DualSense controller
|
|
||||||
/// directly, without any external input library.
|
|
||||||
/// </summary>
|
|
||||||
internal static partial class HidNative
|
|
||||||
{
|
|
||||||
internal const int DigcfPresent = 0x02;
|
|
||||||
internal const int DigcfDeviceInterface = 0x10;
|
|
||||||
internal const uint GenericRead = 0x80000000;
|
|
||||||
internal const uint GenericWrite = 0x40000000;
|
|
||||||
internal const uint FileShareRead = 0x1;
|
|
||||||
internal const uint FileShareWrite = 0x2;
|
|
||||||
internal const uint OpenExisting = 3;
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct SpDeviceInterfaceData
|
|
||||||
{
|
|
||||||
public int CbSize;
|
|
||||||
public Guid InterfaceClassGuid;
|
|
||||||
public int Flags;
|
|
||||||
public nint Reserved;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct HiddAttributes
|
|
||||||
{
|
|
||||||
public int Size;
|
|
||||||
public ushort VendorId;
|
|
||||||
public ushort ProductId;
|
|
||||||
public ushort VersionNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport("hid.dll")]
|
|
||||||
internal static extern void HidD_GetHidGuid(out Guid hidGuid);
|
|
||||||
|
|
||||||
[DllImport("hid.dll")]
|
|
||||||
internal static extern bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
|
|
||||||
|
|
||||||
[DllImport("hid.dll")]
|
|
||||||
internal static extern bool HidD_GetFeature(SafeFileHandle hidDeviceObject, byte[] reportBuffer, int reportBufferLength);
|
|
||||||
|
|
||||||
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
|
|
||||||
internal static extern nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
|
|
||||||
|
|
||||||
[DllImport("setupapi.dll")]
|
|
||||||
internal static extern bool SetupDiEnumDeviceInterfaces(
|
|
||||||
nint deviceInfoSet,
|
|
||||||
nint deviceInfoData,
|
|
||||||
ref Guid interfaceClassGuid,
|
|
||||||
int memberIndex,
|
|
||||||
ref SpDeviceInterfaceData deviceInterfaceData);
|
|
||||||
|
|
||||||
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
|
|
||||||
internal static extern bool SetupDiGetDeviceInterfaceDetail(
|
|
||||||
nint deviceInfoSet,
|
|
||||||
ref SpDeviceInterfaceData deviceInterfaceData,
|
|
||||||
nint deviceInterfaceDetailData,
|
|
||||||
int deviceInterfaceDetailDataSize,
|
|
||||||
out int requiredSize,
|
|
||||||
nint deviceInfoData);
|
|
||||||
|
|
||||||
[DllImport("setupapi.dll")]
|
|
||||||
internal static extern bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
||||||
internal static extern SafeFileHandle CreateFile(
|
|
||||||
string fileName,
|
|
||||||
uint desiredAccess,
|
|
||||||
uint shareMode,
|
|
||||||
nint securityAttributes,
|
|
||||||
uint creationDisposition,
|
|
||||||
uint flagsAndAttributes,
|
|
||||||
nint templateFile);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enumerates the device paths of all present HID interfaces.
|
|
||||||
/// </summary>
|
|
||||||
internal static List<string> EnumerateHidDevicePaths()
|
|
||||||
{
|
|
||||||
var paths = new List<string>();
|
|
||||||
HidD_GetHidGuid(out var hidGuid);
|
|
||||||
var deviceInfoSet = SetupDiGetClassDevs(ref hidGuid, 0, 0, DigcfPresent | DigcfDeviceInterface);
|
|
||||||
if (deviceInfoSet == -1 || deviceInfoSet == 0)
|
|
||||||
{
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var interfaceData = new SpDeviceInterfaceData
|
|
||||||
{
|
|
||||||
CbSize = Marshal.SizeOf<SpDeviceInterfaceData>(),
|
|
||||||
};
|
|
||||||
|
|
||||||
for (var index = 0; SetupDiEnumDeviceInterfaces(deviceInfoSet, 0, ref hidGuid, index, ref interfaceData); index++)
|
|
||||||
{
|
|
||||||
SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref interfaceData, 0, 0, out var requiredSize, 0);
|
|
||||||
if (requiredSize <= 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var detailBuffer = Marshal.AllocHGlobal(requiredSize);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// SP_DEVICE_INTERFACE_DETAIL_DATA_W.cbSize is 8 on x64
|
|
||||||
// (DWORD + aligned WCHAR[1]); the path string follows it.
|
|
||||||
Marshal.WriteInt32(detailBuffer, 8);
|
|
||||||
if (SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref interfaceData, detailBuffer, requiredSize, out _, 0) &&
|
|
||||||
Marshal.PtrToStringUni(detailBuffer + 4) is { Length: > 0 } path)
|
|
||||||
{
|
|
||||||
paths.Add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Marshal.FreeHGlobal(detailBuffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,8 +30,6 @@ public static class PadExports
|
|||||||
public static int PadInit(CpuContext ctx)
|
public static int PadInit(CpuContext ctx)
|
||||||
{
|
{
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
DualSenseReader.EnsureStarted();
|
|
||||||
XInputReader.EnsureStarted();
|
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,13 +59,7 @@ public static class PadExports
|
|||||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
DualSenseReader.EnsureStarted();
|
Console.Error.WriteLine("[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options");
|
||||||
XInputReader.EnsureStarted();
|
|
||||||
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
|
||||||
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
|
||||||
: XInputReader.TryGetState(out _)
|
|
||||||
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
|
|
||||||
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
|
||||||
return ctx.SetReturn(PrimaryPadHandle);
|
return ctx.SetReturn(PrimaryPadHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,128 +170,23 @@ public static class PadExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "yFVnOdGxvZY",
|
|
||||||
ExportName = "scePadSetVibration",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libScePad")]
|
|
||||||
public static int PadSetVibration(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (handle != PrimaryPadHandle)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScePadVibrationParam: { uint8_t largeMotor; uint8_t smallMotor; }
|
|
||||||
Span<byte> parameter = stackalloc byte[2];
|
|
||||||
if (!ctx.Memory.TryRead(parameterAddress, parameter))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
DualSenseReader.SetRumble(parameter[0], parameter[1]);
|
|
||||||
XInputReader.SetRumble(parameter[0], parameter[1]);
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "RR4novUEENY",
|
|
||||||
ExportName = "scePadSetLightBar",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libScePad")]
|
|
||||||
public static int PadSetLightBar(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (handle != PrimaryPadHandle)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScePadColor: { uint8_t r; uint8_t g; uint8_t b; uint8_t reserved; }
|
|
||||||
Span<byte> color = stackalloc byte[4];
|
|
||||||
if (!ctx.Memory.TryRead(parameterAddress, color))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
DualSenseReader.SetLightbar(color[0], color[1], color[2]);
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "DscD1i9HX1w",
|
|
||||||
ExportName = "scePadResetLightBar",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libScePad")]
|
|
||||||
public static int PadResetLightBar(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
if (handle != PrimaryPadHandle)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
DualSenseReader.ResetLightbar();
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
|
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
|
||||||
{
|
{
|
||||||
Span<byte> data = stackalloc byte[PadDataSize];
|
Span<byte> data = stackalloc byte[PadDataSize];
|
||||||
data.Clear();
|
data.Clear();
|
||||||
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
||||||
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
||||||
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
|
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
|
||||||
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
|
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
|
||||||
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
|
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
|
||||||
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
|
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
|
||||||
var l2 = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
|
|
||||||
var r2 = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
|
|
||||||
|
|
||||||
if (DualSenseReader.TryGetState(out var pad))
|
|
||||||
{
|
|
||||||
buttons |= pad.Buttons;
|
|
||||||
// The controller stick wins whenever it is deflected past a
|
|
||||||
// small deadzone; otherwise any keyboard value stays.
|
|
||||||
leftX = MergeAxis(pad.LeftX, leftX);
|
|
||||||
leftY = MergeAxis(pad.LeftY, leftY);
|
|
||||||
rightX = MergeAxis(pad.RightX, rightX);
|
|
||||||
rightY = MergeAxis(pad.RightY, rightY);
|
|
||||||
l2 = Math.Max(l2, pad.L2);
|
|
||||||
r2 = Math.Max(r2, pad.R2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (XInputReader.TryGetState(out var xpad))
|
|
||||||
{
|
|
||||||
buttons |= xpad.Buttons;
|
|
||||||
leftX = MergeAxis(xpad.LeftX, leftX);
|
|
||||||
leftY = MergeAxis(xpad.LeftY, leftY);
|
|
||||||
rightX = MergeAxis(xpad.RightX, rightX);
|
|
||||||
rightY = MergeAxis(xpad.RightY, rightY);
|
|
||||||
l2 = Math.Max(l2, xpad.L2);
|
|
||||||
r2 = Math.Max(r2, xpad.R2);
|
|
||||||
}
|
|
||||||
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
|
||||||
data[0x04] = leftX;
|
data[0x04] = leftX;
|
||||||
data[0x05] = leftY;
|
data[0x05] = leftY;
|
||||||
data[0x06] = rightX;
|
data[0x06] = rightX;
|
||||||
data[0x07] = rightY;
|
data[0x07] = rightY;
|
||||||
data[0x08] = l2;
|
data[0x08] = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
|
||||||
data[0x09] = r2;
|
data[0x09] = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
|
||||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
||||||
data[0x4C] = 1;
|
data[0x4C] = 1;
|
||||||
var timestampTicks = Stopwatch.GetTimestamp();
|
var timestampTicks = Stopwatch.GetTimestamp();
|
||||||
@@ -367,10 +254,4 @@ public static class PadExports
|
|||||||
if (positive && !negative) return 255;
|
if (positive && !negative) return 255;
|
||||||
return 128;
|
return 128;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte MergeAxis(byte controller, byte keyboard)
|
|
||||||
{
|
|
||||||
const int Deadzone = 10;
|
|
||||||
return Math.Abs(controller - 128) > Deadzone ? controller : keyboard;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Snapshot of a physical controller's state, already translated to ORBIS
|
|
||||||
/// pad conventions (SCE_PAD_BUTTON bits; sticks 0..255 with 128 centered
|
|
||||||
/// and Y growing downward; triggers 0..255).
|
|
||||||
/// </summary>
|
|
||||||
internal readonly record struct PadState(
|
|
||||||
bool Connected,
|
|
||||||
uint Buttons,
|
|
||||||
byte LeftX,
|
|
||||||
byte LeftY,
|
|
||||||
byte RightX,
|
|
||||||
byte RightY,
|
|
||||||
byte L2,
|
|
||||||
byte R2);
|
|
||||||
|
|
||||||
/// <summary>SCE_PAD_BUTTON bit values.</summary>
|
|
||||||
internal static class OrbisPadButton
|
|
||||||
{
|
|
||||||
internal const uint L3 = 0x0002;
|
|
||||||
internal const uint R3 = 0x0004;
|
|
||||||
internal const uint Options = 0x0008;
|
|
||||||
internal const uint Up = 0x0010;
|
|
||||||
internal const uint Right = 0x0020;
|
|
||||||
internal const uint Down = 0x0040;
|
|
||||||
internal const uint Left = 0x0080;
|
|
||||||
internal const uint L2 = 0x0100;
|
|
||||||
internal const uint R2 = 0x0200;
|
|
||||||
internal const uint L1 = 0x0400;
|
|
||||||
internal const uint R1 = 0x0800;
|
|
||||||
internal const uint Triangle = 0x1000;
|
|
||||||
internal const uint Circle = 0x2000;
|
|
||||||
internal const uint Cross = 0x4000;
|
|
||||||
internal const uint Square = 0x8000;
|
|
||||||
internal const uint TouchPad = 0x100000;
|
|
||||||
}
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads Xbox 360 / Xbox One (and other XInput-compatible) controllers via
|
|
||||||
/// the Windows XInput API on a background thread, translated to the same
|
|
||||||
/// ORBIS pad conventions as <see cref="DualSenseReader"/>. Supports rumble
|
|
||||||
/// and hot-plug retry; the first connected slot (of four) is used.
|
|
||||||
/// </summary>
|
|
||||||
internal static class XInputReader
|
|
||||||
{
|
|
||||||
private const uint ErrorSuccess = 0;
|
|
||||||
private const int SlotCount = 4;
|
|
||||||
private const byte TriggerThreshold = 30; // XINPUT_GAMEPAD_TRIGGER_THRESHOLD
|
|
||||||
|
|
||||||
// XINPUT_GAMEPAD wButtons bit values.
|
|
||||||
private const ushort XinputDpadUp = 0x0001;
|
|
||||||
private const ushort XinputDpadDown = 0x0002;
|
|
||||||
private const ushort XinputDpadLeft = 0x0004;
|
|
||||||
private const ushort XinputDpadRight = 0x0008;
|
|
||||||
private const ushort XinputStart = 0x0010;
|
|
||||||
private const ushort XinputBack = 0x0020;
|
|
||||||
private const ushort XinputLeftThumb = 0x0040;
|
|
||||||
private const ushort XinputRightThumb = 0x0080;
|
|
||||||
private const ushort XinputLeftShoulder = 0x0100;
|
|
||||||
private const ushort XinputRightShoulder = 0x0200;
|
|
||||||
private const ushort XinputA = 0x1000;
|
|
||||||
private const ushort XinputB = 0x2000;
|
|
||||||
private const ushort XinputX = 0x4000;
|
|
||||||
private const ushort XinputY = 0x8000;
|
|
||||||
|
|
||||||
private static readonly object Gate = new();
|
|
||||||
private static PadState _state;
|
|
||||||
private static bool _started;
|
|
||||||
private static int _slot = -1; // connected XInput user index, -1 when none
|
|
||||||
private static byte _motorLeft;
|
|
||||||
private static byte _motorRight;
|
|
||||||
|
|
||||||
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
|
||||||
internal static void EnsureStarted()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
if (_started)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_started = true;
|
|
||||||
var thread = new Thread(ReadLoop)
|
|
||||||
{
|
|
||||||
IsBackground = true,
|
|
||||||
Name = "XInputReader",
|
|
||||||
};
|
|
||||||
thread.Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool TryGetState(out PadState state)
|
|
||||||
{
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
state = _state;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.Connected;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SetState(in PadState state)
|
|
||||||
{
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
_state = state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Sets rumble; large = left/strong motor, small = right/weak.</summary>
|
|
||||||
internal static void SetRumble(byte largeMotor, byte smallMotor)
|
|
||||||
{
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
if (_motorLeft == largeMotor && _motorRight == smallMotor)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_motorLeft = largeMotor;
|
|
||||||
_motorRight = smallMotor;
|
|
||||||
SendRumbleLocked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SendRumbleLocked()
|
|
||||||
{
|
|
||||||
if (_slot < 0)
|
|
||||||
{
|
|
||||||
return; // resent on connect
|
|
||||||
}
|
|
||||||
|
|
||||||
var vibration = new XInputVibration
|
|
||||||
{
|
|
||||||
LeftMotorSpeed = (ushort)(_motorLeft * 257), // 0..255 -> 0..65535
|
|
||||||
RightMotorSpeed = (ushort)(_motorRight * 257),
|
|
||||||
};
|
|
||||||
_ = XInputSetState((uint)_slot, ref vibration);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReadLoop()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var slot = FindConnectedSlot();
|
|
||||||
if (slot < 0)
|
|
||||||
{
|
|
||||||
SetState(default);
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
_slot = slot;
|
|
||||||
SendRumbleLocked();
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] XInput (Xbox) controller connected.");
|
|
||||||
while (XInputGetState((uint)slot, out var state) == ErrorSuccess)
|
|
||||||
{
|
|
||||||
SetState(Translate(state.Gamepad));
|
|
||||||
Thread.Sleep(8);
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] XInput (Xbox) controller disconnected.");
|
|
||||||
lock (Gate)
|
|
||||||
{
|
|
||||||
_slot = -1;
|
|
||||||
_motorLeft = 0;
|
|
||||||
_motorRight = 0;
|
|
||||||
_state = default;
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (DllNotFoundException)
|
|
||||||
{
|
|
||||||
// XInput unavailable on this system; leave the reader disconnected.
|
|
||||||
}
|
|
||||||
catch (EntryPointNotFoundException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int FindConnectedSlot()
|
|
||||||
{
|
|
||||||
for (var index = 0; index < SlotCount; index++)
|
|
||||||
{
|
|
||||||
if (XInputGetState((uint)index, out _) == ErrorSuccess)
|
|
||||||
{
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PadState Translate(in XInputGamepad pad)
|
|
||||||
{
|
|
||||||
uint buttons = 0;
|
|
||||||
buttons |= (pad.Buttons & XinputDpadUp) != 0 ? OrbisPadButton.Up : 0;
|
|
||||||
buttons |= (pad.Buttons & XinputDpadDown) != 0 ? OrbisPadButton.Down : 0;
|
|
||||||
buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? OrbisPadButton.Left : 0;
|
|
||||||
buttons |= (pad.Buttons & XinputDpadRight) != 0 ? OrbisPadButton.Right : 0;
|
|
||||||
buttons |= (pad.Buttons & XinputStart) != 0 ? OrbisPadButton.Options : 0;
|
|
||||||
buttons |= (pad.Buttons & XinputBack) != 0 ? OrbisPadButton.TouchPad : 0;
|
|
||||||
buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? OrbisPadButton.L3 : 0;
|
|
||||||
buttons |= (pad.Buttons & XinputRightThumb) != 0 ? OrbisPadButton.R3 : 0;
|
|
||||||
buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? OrbisPadButton.L1 : 0;
|
|
||||||
buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? OrbisPadButton.R1 : 0;
|
|
||||||
buttons |= (pad.Buttons & XinputA) != 0 ? OrbisPadButton.Cross : 0;
|
|
||||||
buttons |= (pad.Buttons & XinputB) != 0 ? OrbisPadButton.Circle : 0;
|
|
||||||
buttons |= (pad.Buttons & XinputX) != 0 ? OrbisPadButton.Square : 0;
|
|
||||||
buttons |= (pad.Buttons & XinputY) != 0 ? OrbisPadButton.Triangle : 0;
|
|
||||||
buttons |= pad.LeftTrigger > TriggerThreshold ? OrbisPadButton.L2 : 0;
|
|
||||||
buttons |= pad.RightTrigger > TriggerThreshold ? OrbisPadButton.R2 : 0;
|
|
||||||
|
|
||||||
return new PadState(
|
|
||||||
Connected: true,
|
|
||||||
Buttons: buttons,
|
|
||||||
LeftX: AxisToByte(pad.ThumbLX),
|
|
||||||
LeftY: AxisToByteInverted(pad.ThumbLY),
|
|
||||||
RightX: AxisToByte(pad.ThumbRX),
|
|
||||||
RightY: AxisToByteInverted(pad.ThumbRY),
|
|
||||||
L2: pad.LeftTrigger,
|
|
||||||
R2: pad.RightTrigger);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte AxisToByte(short value) => (byte)((value + 32768) >> 8);
|
|
||||||
|
|
||||||
// XInput Y grows upward, ORBIS pads report Y growing downward.
|
|
||||||
private static byte AxisToByteInverted(short value) => (byte)(255 - ((value + 32768) >> 8));
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
private struct XInputGamepad
|
|
||||||
{
|
|
||||||
public ushort Buttons;
|
|
||||||
public byte LeftTrigger;
|
|
||||||
public byte RightTrigger;
|
|
||||||
public short ThumbLX;
|
|
||||||
public short ThumbLY;
|
|
||||||
public short ThumbRX;
|
|
||||||
public short ThumbRY;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
private struct XInputState
|
|
||||||
{
|
|
||||||
public uint PacketNumber;
|
|
||||||
public XInputGamepad Gamepad;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
private struct XInputVibration
|
|
||||||
{
|
|
||||||
public ushort LeftMotorSpeed;
|
|
||||||
public ushort RightMotorSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// xinput1_4.dll ships with Windows 8 and later.
|
|
||||||
[DllImport("xinput1_4.dll")]
|
|
||||||
private static extern uint XInputGetState(uint userIndex, out XInputState state);
|
|
||||||
|
|
||||||
[DllImport("xinput1_4.dll")]
|
|
||||||
private static extern uint XInputSetState(uint userIndex, ref XInputVibration vibration);
|
|
||||||
}
|
|
||||||
@@ -1253,7 +1253,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
|
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
|
||||||
var enableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1";
|
var enableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1";
|
||||||
byte* validationLayerName = null;
|
byte* validationLayerName = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var applicationInfo = new ApplicationInfo
|
var applicationInfo = new ApplicationInfo
|
||||||
@@ -1313,7 +1313,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
throw new InvalidOperationException("VK_KHR_surface is unavailable.");
|
throw new InvalidOperationException("VK_KHR_surface is unavailable.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validationLayerName is not null && _vk.TryGetInstanceExtension(_instance, out ExtDebugUtils debugUtils))
|
if (validationLayerName is not null && _vk.TryGetInstanceExtension(_instance, out ExtDebugUtils debugUtils))
|
||||||
{
|
{
|
||||||
_debugUtils = debugUtils;
|
_debugUtils = debugUtils;
|
||||||
@@ -1383,7 +1383,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Check(debugUtils.CreateDebugUtilsMessenger(_instance, &messengerInfo, null, out _debugMessenger),
|
Check(debugUtils.CreateDebugUtilsMessenger(_instance, &messengerInfo, null, out _debugMessenger),
|
||||||
"vkCreateDebugUtilsMessengerEXT");
|
"vkCreateDebugUtilsMessengerEXT");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe uint DebugCallback(
|
private static unsafe uint DebugCallback(
|
||||||
DebugUtilsMessageSeverityFlagsEXT severity,
|
DebugUtilsMessageSeverityFlagsEXT severity,
|
||||||
DebugUtilsMessageTypeFlagsEXT type,
|
DebugUtilsMessageTypeFlagsEXT type,
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace SharpEmu.Logging;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Build provenance for the running emulator, populated at compile time from
|
|
||||||
/// GitHub Actions environment variables via <c>[AssemblyMetadata]</c> and
|
|
||||||
/// surfaced as a touchHLE-style banner at the top of the log.
|
|
||||||
/// </summary>
|
|
||||||
public static class BuildInfo
|
|
||||||
{
|
|
||||||
private const string ProjectUrl = "https://github.com/par274/sharpemu";
|
|
||||||
private const string CanonicalRepository = "par274/sharpemu";
|
|
||||||
|
|
||||||
/// <summary>Short commit hash the build was produced from, or <c>null</c>.</summary>
|
|
||||||
public static string? CommitSha { get; }
|
|
||||||
|
|
||||||
/// <summary>Branch the build was produced from, or <c>null</c>.</summary>
|
|
||||||
public static string? Branch { get; }
|
|
||||||
|
|
||||||
/// <summary>Repository slug (<c>owner/name</c>) the build came from, or <c>null</c>.</summary>
|
|
||||||
public static string? Repository { get; }
|
|
||||||
|
|
||||||
/// <summary>URL of the GitHub Actions workflow run that produced the build, or <c>null</c>.</summary>
|
|
||||||
public static string? WorkflowRunUrl { get; }
|
|
||||||
|
|
||||||
/// <summary>Build configuration (e.g. <c>Debug</c> or <c>Release</c>), or <c>null</c>.</summary>
|
|
||||||
public static string? Configuration { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this build is an official release: a Release-configuration CI build
|
|
||||||
/// from the canonical repository, produced by a push to <c>main</c> or a manual
|
|
||||||
/// workflow dispatch (matching the CI <c>release</c> job). All other builds
|
|
||||||
/// (pull requests, forks, feature branches, and local/Debug builds) are
|
|
||||||
/// considered unofficial.
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsOfficialRelease { get; }
|
|
||||||
|
|
||||||
static BuildInfo()
|
|
||||||
{
|
|
||||||
var metadata = ReadMetadata();
|
|
||||||
|
|
||||||
CommitSha = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildSha"));
|
|
||||||
if (CommitSha is { Length: > 7 })
|
|
||||||
{
|
|
||||||
CommitSha = CommitSha[..7];
|
|
||||||
}
|
|
||||||
|
|
||||||
Branch = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildBranch"));
|
|
||||||
Repository = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildRepository"));
|
|
||||||
Configuration = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildConfiguration"));
|
|
||||||
|
|
||||||
var serverUrl = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildServerUrl"));
|
|
||||||
var runId = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildRunId"));
|
|
||||||
if (serverUrl is not null && Repository is not null && runId is not null)
|
|
||||||
{
|
|
||||||
WorkflowRunUrl = $"{serverUrl.TrimEnd('/')}/{Repository}/actions/runs/{runId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
var eventName = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildEventName"));
|
|
||||||
var gitRef = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildRef"));
|
|
||||||
|
|
||||||
var isReleaseConfig = string.Equals(Configuration, "Release", StringComparison.OrdinalIgnoreCase);
|
|
||||||
var isCanonicalRepo = string.Equals(Repository, CanonicalRepository, StringComparison.OrdinalIgnoreCase);
|
|
||||||
var isReleaseTrigger =
|
|
||||||
string.Equals(eventName, "workflow_dispatch", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
(string.Equals(eventName, "push", StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
string.Equals(gitRef, "refs/heads/main", StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
IsOfficialRelease = isReleaseConfig && isCanonicalRepo && isReleaseTrigger && CommitSha is not null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The multi-line banner, e.g.
|
|
||||||
/// <code>
|
|
||||||
/// SharpEmu UNOFFICIAL f11ac59 — https://github.com/par274/sharpemu
|
|
||||||
///
|
|
||||||
/// Built from branch "main" of "par274/sharpemu" by GitHub Actions workflow run https://github.com/par274/sharpemu/actions/runs/123.
|
|
||||||
/// </code>
|
|
||||||
/// Official release builds drop the <c>UNOFFICIAL</c> tag. Falls back to a
|
|
||||||
/// local-build line when no CI provenance is present.
|
|
||||||
/// </summary>
|
|
||||||
public static string Banner
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
string version;
|
|
||||||
if (CommitSha is null)
|
|
||||||
{
|
|
||||||
version = "UNOFFICIAL";
|
|
||||||
}
|
|
||||||
else if (IsOfficialRelease)
|
|
||||||
{
|
|
||||||
version = CommitSha;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
version = $"UNOFFICIAL {CommitSha}";
|
|
||||||
}
|
|
||||||
|
|
||||||
var header = $"SharpEmu {version} — {ProjectUrl}";
|
|
||||||
|
|
||||||
if (Branch is null || Repository is null || WorkflowRunUrl is null)
|
|
||||||
{
|
|
||||||
return $"{header}{Environment.NewLine}{Environment.NewLine}Local build (not produced by CI).";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{header}{Environment.NewLine}{Environment.NewLine}" +
|
|
||||||
$"Built from branch \"{Branch}\" of \"{Repository}\" by GitHub Actions workflow run {WorkflowRunUrl}.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Dictionary<string, string> ReadMetadata()
|
|
||||||
{
|
|
||||||
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
||||||
var assemblies = new[] { Assembly.GetEntryAssembly(), typeof(BuildInfo).Assembly };
|
|
||||||
|
|
||||||
foreach (var assembly in assemblies)
|
|
||||||
{
|
|
||||||
if (assembly is null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var attribute in assembly.GetCustomAttributes<AssemblyMetadataAttribute>())
|
|
||||||
{
|
|
||||||
if (attribute.Key.StartsWith("SharpEmu.Build", StringComparison.Ordinal) &&
|
|
||||||
!result.ContainsKey(attribute.Key) &&
|
|
||||||
attribute.Value is not null)
|
|
||||||
{
|
|
||||||
result[attribute.Key] = attribute.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? Normalize(string? value) =>
|
|
||||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
namespace SharpEmu.Logging;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dispatches each <see cref="LogEntry"/> to every child sink.
|
|
||||||
/// Exceptions thrown by a child are swallowed so one failing sink
|
|
||||||
/// (e.g. a closed file) cannot silence the others.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class CompositeLogSink : ISharpEmuLogSink, IDisposable
|
|
||||||
{
|
|
||||||
private readonly ISharpEmuLogSink[] _sinks;
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
public CompositeLogSink(params ISharpEmuLogSink[] sinks)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(sinks);
|
|
||||||
|
|
||||||
foreach (var sink in sinks)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(sink, nameof(sinks));
|
|
||||||
}
|
|
||||||
|
|
||||||
_sinks = sinks;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyList<ISharpEmuLogSink> Sinks => _sinks;
|
|
||||||
|
|
||||||
public void Write(in LogEntry entry)
|
|
||||||
{
|
|
||||||
foreach (var sink in _sinks)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
sink.Write(in entry);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// A broken sink must not prevent the remaining sinks from logging.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
|
|
||||||
foreach (var sink in _sinks)
|
|
||||||
{
|
|
||||||
if (sink is IDisposable disposable)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace SharpEmu.Logging;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes log entries to a file. Thread-safe via an internal lock.
|
|
||||||
/// <see cref="StreamWriter.AutoFlush"/> is enabled so entries survive a crash.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
|
|
||||||
{
|
|
||||||
private readonly object _sync = new();
|
|
||||||
private readonly StreamWriter _writer;
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
/// <param name="path">Absolute or relative file path. Parent directories are created if missing.</param>
|
|
||||||
/// <param name="append"><see langword="true"/> to append to an existing file; <see langword="false"/> to overwrite.</param>
|
|
||||||
/// <param name="includeTimestamp">Always recommended for file logs — entries include a full date-time prefix.</param>
|
|
||||||
public FileLogSink(string path, bool append = true, bool includeTimestamp = true)
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(path);
|
|
||||||
|
|
||||||
var directory = Path.GetDirectoryName(path);
|
|
||||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileStream = new FileStream(
|
|
||||||
path,
|
|
||||||
append ? FileMode.Append : FileMode.Create,
|
|
||||||
FileAccess.Write,
|
|
||||||
FileShare.Read,
|
|
||||||
bufferSize: 4096,
|
|
||||||
FileOptions.SequentialScan);
|
|
||||||
_writer = new StreamWriter(fileStream, Encoding.UTF8)
|
|
||||||
{
|
|
||||||
AutoFlush = true
|
|
||||||
};
|
|
||||||
|
|
||||||
IncludeTimestamp = includeTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IncludeTimestamp { get; set; }
|
|
||||||
|
|
||||||
public void Write(in LogEntry entry)
|
|
||||||
{
|
|
||||||
lock (_sync)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IncludeTimestamp)
|
|
||||||
{
|
|
||||||
_writer.Write('[');
|
|
||||||
_writer.Write(entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
|
||||||
_writer.Write(']');
|
|
||||||
}
|
|
||||||
|
|
||||||
_writer.Write('[');
|
|
||||||
_writer.Write(ToLevelLabel(entry.Level));
|
|
||||||
_writer.Write(']');
|
|
||||||
_writer.Write('[');
|
|
||||||
_writer.Write(entry.Category);
|
|
||||||
_writer.Write(']');
|
|
||||||
_writer.Write(' ');
|
|
||||||
|
|
||||||
_writer.Write(entry.SourceFileName);
|
|
||||||
if (entry.SourceLine > 0)
|
|
||||||
{
|
|
||||||
_writer.Write(':');
|
|
||||||
_writer.Write(entry.SourceLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
_writer.Write(' ');
|
|
||||||
_writer.WriteLine(entry.Message);
|
|
||||||
|
|
||||||
if (entry.Exception is not null)
|
|
||||||
{
|
|
||||||
_writer.WriteLine(entry.Exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
lock (_sync)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
_writer.Flush();
|
|
||||||
_writer.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ToLevelLabel(LogLevel level) => level switch
|
|
||||||
{
|
|
||||||
LogLevel.Trace => "TRACE",
|
|
||||||
LogLevel.Debug => "DEBUG",
|
|
||||||
LogLevel.Info => "INFO",
|
|
||||||
LogLevel.Warning => "WARNING",
|
|
||||||
LogLevel.Error => "ERROR",
|
|
||||||
LogLevel.Critical => "CRITICAL",
|
|
||||||
_ => "LOG",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -12,14 +12,10 @@ public static class SharpEmuLog
|
|||||||
new(StringComparer.Ordinal);
|
new(StringComparer.Ordinal);
|
||||||
private static readonly object ConfigurationSync = new();
|
private static readonly object ConfigurationSync = new();
|
||||||
private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment();
|
private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment();
|
||||||
private static bool _fileCapturesAllLevels;
|
private static ISharpEmuLogSink _sink = new ConsoleLogSink(
|
||||||
private static ISharpEmuLogSink _sink = ResolveSinkFromEnvironment();
|
useColors: ResolveColorEnabledFromEnvironment(),
|
||||||
|
includeTimestamp: false);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Entries below this level are dropped. When a SHARPEMU_LOG_FILE sink is
|
|
||||||
/// active it only limits the console — the file receives every level.
|
|
||||||
/// <see cref="LogLevel.None"/> disables logging entirely, file included.
|
|
||||||
/// </summary>
|
|
||||||
public static LogLevel MinimumLevel
|
public static LogLevel MinimumLevel
|
||||||
{
|
{
|
||||||
get => _minimumLevel;
|
get => _minimumLevel;
|
||||||
@@ -41,31 +37,11 @@ public static class SharpEmuLog
|
|||||||
ArgumentNullException.ThrowIfNull(value);
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
lock (ConfigurationSync)
|
lock (ConfigurationSync)
|
||||||
{
|
{
|
||||||
if (ReferenceEquals(_sink, value))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A replacement sink is not the environment-configured
|
|
||||||
// console+file pair, so the minimum level applies globally
|
|
||||||
// again.
|
|
||||||
_fileCapturesAllLevels = false;
|
|
||||||
|
|
||||||
if (_sink is IDisposable disposable)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_sink = value;
|
_sink = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Configure(LogLevel? minimumLevel = null, ISharpEmuLogSink? sink = null)
|
public static void Configure(LogLevel? minimumLevel = null, ISharpEmuLogSink? sink = null)
|
||||||
{
|
{
|
||||||
if (minimumLevel.HasValue)
|
if (minimumLevel.HasValue)
|
||||||
@@ -79,22 +55,6 @@ public static class SharpEmuLog
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disposes the active sink if it implements <see cref="IDisposable"/>.
|
|
||||||
/// Call at shutdown to flush file buffers. Logging after this call
|
|
||||||
/// continues to work for non-disposable sinks (e.g. <see cref="ConsoleLogSink"/>).
|
|
||||||
/// </summary>
|
|
||||||
public static void Shutdown()
|
|
||||||
{
|
|
||||||
lock (ConfigurationSync)
|
|
||||||
{
|
|
||||||
if (_sink is IDisposable disposable)
|
|
||||||
{
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SharpEmuLogger For(string category)
|
public static SharpEmuLogger For(string category)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(category);
|
ArgumentException.ThrowIfNullOrWhiteSpace(category);
|
||||||
@@ -133,14 +93,7 @@ public static class SharpEmuLog
|
|||||||
internal static bool IsEnabled(LogLevel level)
|
internal static bool IsEnabled(LogLevel level)
|
||||||
{
|
{
|
||||||
var minimum = _minimumLevel;
|
var minimum = _minimumLevel;
|
||||||
if (minimum == LogLevel.None)
|
return minimum != LogLevel.None && level >= minimum;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// With a file sink capturing all levels, the console filter is
|
|
||||||
// applied per-sink instead of here.
|
|
||||||
return _fileCapturesAllLevels || level >= minimum;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void Write(
|
internal static void Write(
|
||||||
@@ -193,52 +146,6 @@ public static class SharpEmuLog
|
|||||||
return !IsTrueLike(raw);
|
return !IsTrueLike(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ISharpEmuLogSink ResolveSinkFromEnvironment()
|
|
||||||
{
|
|
||||||
var consoleSink = new ConsoleLogSink(
|
|
||||||
useColors: ResolveColorEnabledFromEnvironment(),
|
|
||||||
includeTimestamp: false);
|
|
||||||
|
|
||||||
var logFilePath = Environment.GetEnvironmentVariable("SHARPEMU_LOG_FILE");
|
|
||||||
if (!string.IsNullOrWhiteSpace(logFilePath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileSink = new FileLogSink(logFilePath, append: true, includeTimestamp: true);
|
|
||||||
// The file gets every level; the configured minimum only
|
|
||||||
// limits what reaches the console.
|
|
||||||
_fileCapturesAllLevels = true;
|
|
||||||
return new CompositeLogSink(new MinimumLevelFilterSink(consoleSink), fileSink);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Bootstrapping — the logging system is not yet active, so stderr is the only channel.
|
|
||||||
Console.Error.WriteLine($"[SHARPEMU_LOG] Failed to open log file '{logFilePath}': {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return consoleSink;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Forwards only entries at or above <see cref="MinimumLevel"/>. Wraps
|
|
||||||
/// the console sink when a file sink captures all levels.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class MinimumLevelFilterSink : ISharpEmuLogSink
|
|
||||||
{
|
|
||||||
private readonly ISharpEmuLogSink _inner;
|
|
||||||
|
|
||||||
internal MinimumLevelFilterSink(ISharpEmuLogSink inner) => _inner = inner;
|
|
||||||
|
|
||||||
public void Write(in LogEntry entry)
|
|
||||||
{
|
|
||||||
if (entry.Level >= _minimumLevel)
|
|
||||||
{
|
|
||||||
_inner.Write(in entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsTrueLike(string? text)
|
private static bool IsTrueLike(string? text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
|||||||
Reference in New Issue
Block a user