mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e604fb606d | |||
| df53ff59d9 | |||
| 4c35831cb8 |
@@ -12,11 +12,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
|
||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -12,4 +12,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
@@ -1499,6 +1499,7 @@ public partial class MainWindow : Window
|
||||
2 => "Exit.EbootNotFound",
|
||||
3 => "Exit.RuntimeException",
|
||||
4 => "Exit.EmulationError",
|
||||
-1073741819 => "Exit.EmulationError",
|
||||
_ => "Exit.Unknown",
|
||||
};
|
||||
var meaning = Localization.Instance.Get(meaningKey);
|
||||
|
||||
@@ -54,20 +54,41 @@ internal static class PakDirectoryTracker
|
||||
|
||||
if (state.Directory is { } entries)
|
||||
{
|
||||
// Several unrelated archived files can share a byte count (e.g. a head model and a bot
|
||||
// navigation file both 0x3A34 bytes). Directory order alone then picks whichever comes
|
||||
// first, which is wrong when the game reads them out of order. id archives cluster
|
||||
// related assets, and the guest streams them with locality, so disambiguate collisions
|
||||
// by choosing the unconsumed match nearest the running read cursor.
|
||||
DirectoryEntry? best = null;
|
||||
var bestDistance = ulong.MaxValue;
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (!entry.Consumed && entry.FileLen == requestedSize)
|
||||
if (entry.Consumed || entry.FileLen != requestedSize)
|
||||
{
|
||||
entry.Consumed = true;
|
||||
if (_trace)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pak.directory_match: id=0x{fileId:X8} name='{entry.Name}' " +
|
||||
$"filepos=0x{entry.FilePos:X8} filelen=0x{entry.FileLen:X8}");
|
||||
}
|
||||
|
||||
return entry.FilePos;
|
||||
continue;
|
||||
}
|
||||
|
||||
var distance = entry.FilePos >= state.NextOffset
|
||||
? entry.FilePos - state.NextOffset
|
||||
: state.NextOffset - entry.FilePos;
|
||||
if (distance < bestDistance)
|
||||
{
|
||||
bestDistance = distance;
|
||||
best = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (best is not null)
|
||||
{
|
||||
best.Consumed = true;
|
||||
if (_trace)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pak.directory_match: id=0x{fileId:X8} name='{best.Name}' " +
|
||||
$"filepos=0x{best.FilePos:X8} filelen=0x{best.FileLen:X8}");
|
||||
}
|
||||
|
||||
return best.FilePos;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,30 @@ public static class JsonExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// sce::Json::Initializer::setGlobalNullAccessCallback(const Value& (*)(ValueType, const Value*, void*), void*)
|
||||
// Registers the guest hook invoked when a Value is accessed as the wrong type. Quake calls it
|
||||
// during kexPSNWebAPI::Initialize and treats a non-zero return as a fatal init failure.
|
||||
[SysAbiExport(
|
||||
Nid = "+drDFyAS6u4",
|
||||
ExportName = "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceJson")]
|
||||
public static int InitializerSetGlobalNullAccessCallback(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
if (thisAddress == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
JsonObjectHeap.GlobalNullAccessCallback = ctx[CpuRegister.Rsi];
|
||||
JsonObjectHeap.GlobalNullAccessCallbackContext = ctx[CpuRegister.Rdx];
|
||||
TraceJson("Initializer.setGlobalNullAccessCallback", thisAddress, ctx[CpuRegister.Rsi]);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WSOuge5IsCg",
|
||||
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Json;
|
||||
|
||||
// sce::Json::Value and sce::Json::String constructors, setters and destructors. Prospero titles
|
||||
// (Quake among them) build a Value tree and populate it through these before serializing it for a
|
||||
// web request; without them the imports resolve to nothing and the guest faults on the call. The
|
||||
// payload is modelled in JsonObjectHeap; here we only translate the C++ ABI (registers in, `this`
|
||||
// back out) and never write the guest object, so a stack-allocated Value/String is left intact.
|
||||
//
|
||||
// Only the complete-object variants (C1/D1) are bound — those are what standalone locals emit and
|
||||
// what the observed Prospero imports use. The base-object variants (C2/D2) are left for a title
|
||||
// that actually imports them.
|
||||
public static class JsonValueExports
|
||||
{
|
||||
private const int MaxStringLength = 0x10000;
|
||||
|
||||
private static int ReturnThis(CpuContext ctx, ulong thisAddress)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = thisAddress;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int ReturnVoid(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static double ReadDoubleArg(CpuContext ctx)
|
||||
{
|
||||
ctx.GetXmmRegister(0, out var low, out _);
|
||||
return BitConverter.Int64BitsToDouble(unchecked((long)low));
|
||||
}
|
||||
|
||||
private static string ReadCString(CpuContext ctx, ulong address) =>
|
||||
ctx.TryReadNullTerminatedUtf8(address, MaxStringLength, out var text) ? text : string.Empty;
|
||||
|
||||
// ---- sce::Json::Value constructors ----
|
||||
|
||||
[SysAbiExport(Nid = "qBMjqyBn3OM", ExportName = "_ZN3sce4Json5ValueC1Ev",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueDefaultConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.Null);
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "UeuWT+yNdCQ", ExportName = "_ZN3sce4Json5ValueC1Eb",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueBooleanConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromBoolean((ctx[CpuRegister.Rsi] & 0xFF) != 0));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "0lLK8+kDqmE", ExportName = "_ZN3sce4Json5ValueC1El",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueIntegerConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromInteger(unchecked((long)ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "x4AUdbhpRB0", ExportName = "_ZN3sce4Json5ValueC1Em",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueUnsignedConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromUnsignedInteger(ctx[CpuRegister.Rsi]));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "sOmU4vnx3s0", ExportName = "_ZN3sce4Json5ValueC1Ed",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueRealConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromReal(ReadDoubleArg(ctx)));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "b9V6fmppLXY", ExportName = "_ZN3sce4Json5ValueC1EPKc",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueCStringConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(ReadCString(ctx, ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "CbrT3dwDILo", ExportName = "_ZN3sce4Json5ValueC1ENS0_9ValueTypeE",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueTypeConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromExplicitType(unchecked((uint)ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "sZIoMRGO+jk", ExportName = "_ZN3sce4Json5ValueC1ERKNS0_6StringE",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueStringConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "WTtYf+cNnXI", ExportName = "_ZN3sce4Json5ValueD1Ev",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueDestructor(CpuContext ctx)
|
||||
{
|
||||
JsonObjectHeap.RemoveValue(ctx[CpuRegister.Rdi]);
|
||||
return ReturnVoid(ctx);
|
||||
}
|
||||
|
||||
// ---- sce::Json::Value setters (return Value&, i.e. `this`) ----
|
||||
|
||||
[SysAbiExport(Nid = "5yHuiWXo2gg", ExportName = "_ZN3sce4Json5Value3setEb",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetBoolean(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromBoolean((ctx[CpuRegister.Rsi] & 0xFF) != 0));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "QxVVYhP-mvg", ExportName = "_ZN3sce4Json5Value3setEl",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetInteger(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromInteger(unchecked((long)ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "SIe1ZmW7e7s", ExportName = "_ZN3sce4Json5Value3setEm",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetUnsigned(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromUnsignedInteger(ctx[CpuRegister.Rsi]));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "BSmWDIkV4w4", ExportName = "_ZN3sce4Json5Value3setEd",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetReal(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromReal(ReadDoubleArg(ctx)));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "IKQimvG9Wqs", ExportName = "_ZN3sce4Json5Value3setENS0_9ValueTypeE",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetType(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromExplicitType(unchecked((uint)ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "n6FC+l9DU70", ExportName = "_ZN3sce4Json5Value3setEPKc",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetCString(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(ReadCString(ctx, ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "6l3Bv2gysNc", ExportName = "_ZN3sce4Json5Value3setERKNS0_6StringE",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetString(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "FIjXN2TkuTs", ExportName = "_ZN3sce4Json5Value5clearEv",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueClear(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.Null);
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
// ---- sce::Json::String ----
|
||||
|
||||
[SysAbiExport(Nid = "9KUZFjI1IxA", ExportName = "_ZN3sce4Json6StringC1EPKc",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int StringCStringConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetString(thisAddress, ReadCString(ctx, ctx[CpuRegister.Rsi]));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "qSmqLXXCPas", ExportName = "_ZN3sce4Json6StringC1Ev",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int StringDefaultConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetString(thisAddress, string.Empty);
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "0CAesfH963Q", ExportName = "_ZN3sce4Json6StringC1ERKS1_",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int StringCopyConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetString(thisAddress, JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi]));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "cG1VE2HMl6c", ExportName = "_ZN3sce4Json6StringD1Ev",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int StringDestructor(CpuContext ctx)
|
||||
{
|
||||
JsonObjectHeap.RemoveString(ctx[CpuRegister.Rdi]);
|
||||
return ReturnVoid(ctx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace SharpEmu.Libs.Json;
|
||||
|
||||
// sce::Json::Value is an opaque variant type (null / bool / signed / unsigned / real / string /
|
||||
// array / object). Games build one, populate it through set()/ctors and later serialize it. We
|
||||
// model the payload host-side keyed by the guest `this` pointer instead of writing into the guest
|
||||
// object: the object is often stack-allocated and its real byte layout is unknown, so writing a
|
||||
// guessed layout risks smashing an adjacent stack canary (the same failure the AudioOut2 context
|
||||
// param sizing note in this project already ran into). The guest reaches the payload only through
|
||||
// libSceJson methods, so shadowing by address is enough for the build path.
|
||||
internal enum JsonValueKind : byte
|
||||
{
|
||||
Null = 0,
|
||||
Boolean = 1,
|
||||
Integer = 2,
|
||||
UInteger = 3,
|
||||
Real = 4,
|
||||
String = 5,
|
||||
|
||||
// set(ValueType) / Value(ValueType): the guest chose the type itself. We keep its raw enum
|
||||
// value verbatim rather than mapping it, because the canonical ValueType constants are not
|
||||
// known from clean-room evidence and round-tripping the guest's own value is what matters.
|
||||
ExplicitType = 6,
|
||||
}
|
||||
|
||||
internal readonly struct JsonValueState
|
||||
{
|
||||
private JsonValueState(
|
||||
JsonValueKind kind,
|
||||
bool boolean = false,
|
||||
long integer = 0,
|
||||
ulong unsignedInteger = 0,
|
||||
double real = 0,
|
||||
string? text = null,
|
||||
uint explicitType = 0)
|
||||
{
|
||||
Kind = kind;
|
||||
Boolean = boolean;
|
||||
Integer = integer;
|
||||
UnsignedInteger = unsignedInteger;
|
||||
Real = real;
|
||||
Text = text;
|
||||
ExplicitType = explicitType;
|
||||
}
|
||||
|
||||
public JsonValueKind Kind { get; }
|
||||
public bool Boolean { get; }
|
||||
public long Integer { get; }
|
||||
public ulong UnsignedInteger { get; }
|
||||
public double Real { get; }
|
||||
public string? Text { get; }
|
||||
public uint ExplicitType { get; }
|
||||
|
||||
public static JsonValueState Null { get; } = new(JsonValueKind.Null);
|
||||
|
||||
public static JsonValueState FromBoolean(bool value) => new(JsonValueKind.Boolean, boolean: value);
|
||||
|
||||
public static JsonValueState FromInteger(long value) => new(JsonValueKind.Integer, integer: value);
|
||||
|
||||
public static JsonValueState FromUnsignedInteger(ulong value) =>
|
||||
new(JsonValueKind.UInteger, unsignedInteger: value);
|
||||
|
||||
public static JsonValueState FromReal(double value) => new(JsonValueKind.Real, real: value);
|
||||
|
||||
public static JsonValueState FromString(string value) => new(JsonValueKind.String, text: value);
|
||||
|
||||
public static JsonValueState FromExplicitType(uint value) =>
|
||||
new(JsonValueKind.ExplicitType, explicitType: value);
|
||||
}
|
||||
|
||||
// Shared host-side heap for the libSceJson object shadows. Keyed by the guest object address;
|
||||
// constructors overwrite and destructors remove, so guest stack-address reuse stays correct.
|
||||
internal static class JsonObjectHeap
|
||||
{
|
||||
public static ConcurrentDictionary<ulong, JsonValueState> Values { get; } = new();
|
||||
|
||||
public static ConcurrentDictionary<ulong, string> Strings { get; } = new();
|
||||
|
||||
// Guest function the library should call when a Value is read as the wrong type. This HLE
|
||||
// never dereferences missing members (shadows degrade to defaults), so the hook is stored for
|
||||
// fidelity but not invoked.
|
||||
public static ulong GlobalNullAccessCallback;
|
||||
|
||||
public static ulong GlobalNullAccessCallbackContext;
|
||||
|
||||
public static void SetValue(ulong address, JsonValueState state) => Values[address] = state;
|
||||
|
||||
public static void RemoveValue(ulong address) => Values.TryRemove(address, out _);
|
||||
|
||||
public static void SetString(ulong address, string text) => Strings[address] = text;
|
||||
|
||||
public static void RemoveString(ulong address) => Strings.TryRemove(address, out _);
|
||||
|
||||
// A missing shadow (temporary the compiler built without an out-of-line ctor, or a copy we did
|
||||
// not track) degrades to the empty string rather than faulting.
|
||||
public static string GetStringOrEmpty(ulong address) =>
|
||||
Strings.TryGetValue(address, out var text) ? text : string.Empty;
|
||||
|
||||
internal static void ResetForTests()
|
||||
{
|
||||
Values.Clear();
|
||||
Strings.Clear();
|
||||
GlobalNullAccessCallback = 0;
|
||||
GlobalNullAccessCallbackContext = 0;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Silk.NET.Vulkan" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Ampr;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Ampr;
|
||||
|
||||
// PakDirectoryTracker reconstructs the absolute pak offset for a "next sequential chunk" read
|
||||
// (offset -1) from the requested byte count. When several archived files share that byte count,
|
||||
// picking by directory order alone mis-resolves out-of-order reads: Quake requested progs/h_ogre.mdl
|
||||
// (0x3A34 bytes) but the tracker returned bots/navigation/death32c.nav, which shares the size and
|
||||
// sits earlier in the directory, so the guest parsed NAV2 data as a brush model and aborted.
|
||||
public sealed class PakDirectoryTrackerTests
|
||||
{
|
||||
private const int EntrySize = 64;
|
||||
private const int NameLength = 56;
|
||||
|
||||
private readonly record struct PakEntry(string Name, uint FilePos, uint FileLen);
|
||||
|
||||
[Fact]
|
||||
public void ResolveSequentialOffset_SizeCollision_PicksEntryNearestReadCursor()
|
||||
{
|
||||
const uint fileId = 0x5AA5_0001;
|
||||
const uint collidingLen = 0x3A34;
|
||||
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
// "far" collides in size but lives early in the pak; "near" is what the guest actually wants.
|
||||
LoadDirectory(ctx, fileId, memory, 0x1_0000_0000, dirFileOffset: 0x400, new[]
|
||||
{
|
||||
new PakEntry("far.dat", FilePos: 0x1000, FileLen: collidingLen),
|
||||
new PakEntry("near.dat", FilePos: 0x9000, FileLen: collidingLen),
|
||||
});
|
||||
|
||||
// Advance the read cursor next to the "near" entry, mimicking a burst of nearby asset reads.
|
||||
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: 0x8800, bytesRead: 0x400);
|
||||
|
||||
Assert.Equal(0x9000UL, PakDirectoryTracker.ResolveSequentialOffset(fileId, collidingLen));
|
||||
// Once the near entry is consumed, the same size resolves to the remaining match.
|
||||
Assert.Equal(0x1000UL, PakDirectoryTracker.ResolveSequentialOffset(fileId, collidingLen));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveSequentialOffset_ContiguousSameSizeRun_StaysInOrder()
|
||||
{
|
||||
const uint fileId = 0x5AA5_0002;
|
||||
const uint runLen = 0x608;
|
||||
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
// A contiguous run of equal-size lumps (like Quake's gfx/weapons/ww_*.lmp) must still resolve
|
||||
// in packed order when the guest streams them sequentially.
|
||||
LoadDirectory(ctx, fileId, memory, 0x1_0000_0000, dirFileOffset: 0x400, new[]
|
||||
{
|
||||
new PakEntry("lump_a", FilePos: 0x1000, FileLen: runLen),
|
||||
new PakEntry("lump_b", FilePos: 0x1000 + runLen, FileLen: runLen),
|
||||
new PakEntry("lump_c", FilePos: 0x1000 + (runLen * 2), FileLen: runLen),
|
||||
});
|
||||
|
||||
var first = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
|
||||
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: first, bytesRead: runLen);
|
||||
var second = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
|
||||
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: second, bytesRead: runLen);
|
||||
var third = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
|
||||
|
||||
Assert.Equal(0x1000UL, first);
|
||||
Assert.Equal(0x1000UL + runLen, second);
|
||||
Assert.Equal(0x1000UL + (runLen * 2), third);
|
||||
}
|
||||
|
||||
// Feeds the tracker a synthetic PACK header + directory table exactly as the AMPR read path does:
|
||||
// first the 12-byte header (which arms directory parsing), then the directory records themselves.
|
||||
private static void LoadDirectory(
|
||||
CpuContext ctx,
|
||||
uint fileId,
|
||||
FakeCpuMemory memory,
|
||||
ulong destination,
|
||||
ulong dirFileOffset,
|
||||
PakEntry[] entries)
|
||||
{
|
||||
Span<byte> header = stackalloc byte[12];
|
||||
header[0] = (byte)'P';
|
||||
header[1] = (byte)'A';
|
||||
header[2] = (byte)'C';
|
||||
header[3] = (byte)'K';
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header[4..8], (uint)dirFileOffset);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header[8..12], (uint)(entries.Length * EntrySize));
|
||||
memory.TryWrite(destination, header);
|
||||
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset: 0, bytesRead: 12);
|
||||
|
||||
var table = new byte[entries.Length * EntrySize];
|
||||
for (var i = 0; i < entries.Length; i++)
|
||||
{
|
||||
var record = table.AsSpan(i * EntrySize, EntrySize);
|
||||
var name = Encoding.ASCII.GetBytes(entries[i].Name);
|
||||
name.AsSpan(0, Math.Min(name.Length, NameLength)).CopyTo(record);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(record.Slice(56, 4), entries[i].FilePos);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(record.Slice(60, 4), entries[i].FileLen);
|
||||
}
|
||||
|
||||
memory.TryWrite(destination, table);
|
||||
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, dirFileOffset, (ulong)table.Length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Tests;
|
||||
|
||||
// A single contiguous guest region backed by a byte[]. Enough to hand C strings and small
|
||||
// structures to HLE exports under test without a live guest.
|
||||
internal sealed class FakeCpuMemory : ICpuMemory
|
||||
{
|
||||
private readonly ulong _base;
|
||||
private readonly byte[] _storage;
|
||||
|
||||
public FakeCpuMemory(ulong baseAddress, int size)
|
||||
{
|
||||
_base = baseAddress;
|
||||
_storage = new byte[size];
|
||||
}
|
||||
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
if (!TryResolve(virtualAddress, destination.Length, out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (!TryResolve(virtualAddress, source.Length, out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
source.CopyTo(_storage.AsSpan(offset, source.Length));
|
||||
return true;
|
||||
}
|
||||
|
||||
public ulong WriteCString(ulong virtualAddress, string text)
|
||||
{
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(text);
|
||||
TryWrite(virtualAddress, bytes);
|
||||
TryWrite(virtualAddress + (ulong)bytes.Length, stackalloc byte[] { 0 });
|
||||
return virtualAddress;
|
||||
}
|
||||
|
||||
private bool TryResolve(ulong virtualAddress, int length, out int offset)
|
||||
{
|
||||
offset = 0;
|
||||
if (virtualAddress < _base)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var relative = virtualAddress - _base;
|
||||
if (relative + (ulong)length > (ulong)_storage.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = (int)relative;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Json;
|
||||
|
||||
// These NIDs came back "unresolved" in the Quake (PPSA01880) import log right before its
|
||||
// access violation. This asserts they now resolve to the Json handlers and dispatch cleanly,
|
||||
// which is the plumbing the direct-call tests cannot cover.
|
||||
[Collection("JsonObjectHeap")]
|
||||
public sealed class JsonExportRegistrationTests
|
||||
{
|
||||
private static readonly (string Nid, string Name)[] ExpectedExports =
|
||||
{
|
||||
("qBMjqyBn3OM", "_ZN3sce4Json5ValueC1Ev"),
|
||||
("5yHuiWXo2gg", "_ZN3sce4Json5Value3setEb"),
|
||||
("QxVVYhP-mvg", "_ZN3sce4Json5Value3setEl"),
|
||||
("SIe1ZmW7e7s", "_ZN3sce4Json5Value3setEm"),
|
||||
("BSmWDIkV4w4", "_ZN3sce4Json5Value3setEd"),
|
||||
("IKQimvG9Wqs", "_ZN3sce4Json5Value3setENS0_9ValueTypeE"),
|
||||
("6l3Bv2gysNc", "_ZN3sce4Json5Value3setERKNS0_6StringE"),
|
||||
("9KUZFjI1IxA", "_ZN3sce4Json6StringC1EPKc"),
|
||||
("cG1VE2HMl6c", "_ZN3sce4Json6StringD1Ev"),
|
||||
("+drDFyAS6u4", "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_"),
|
||||
};
|
||||
|
||||
private static ModuleManager CreateRegisteredManager()
|
||||
{
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterFromAssembly(typeof(JsonValueExports).Assembly, Generation.Gen5);
|
||||
return manager;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuakeUnresolvedJsonNids_ResolveToJsonExports()
|
||||
{
|
||||
var manager = CreateRegisteredManager();
|
||||
|
||||
foreach (var (nid, name) in ExpectedExports)
|
||||
{
|
||||
Assert.True(manager.TryGetExport(nid, out var export), $"NID {nid} did not register.");
|
||||
Assert.Equal(name, export.Name);
|
||||
Assert.Equal("libSceJson", export.LibraryName);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetGlobalNullAccessCallback_StoresHookAndReturnsOk()
|
||||
{
|
||||
JsonObjectHeap.ResetForTests();
|
||||
var manager = CreateRegisteredManager();
|
||||
var ctx = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
|
||||
ctx[CpuRegister.Rdi] = 0x1_0000_0000; // Initializer instance
|
||||
ctx[CpuRegister.Rsi] = 0x8_0012_3456; // guest callback
|
||||
ctx[CpuRegister.Rdx] = 0x1_0000_0800; // user context
|
||||
|
||||
Assert.True(manager.TryDispatch("+drDFyAS6u4", ctx, out var result));
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||
Assert.Equal(0UL, ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(0x8_0012_3456UL, JsonObjectHeap.GlobalNullAccessCallback);
|
||||
Assert.Equal(0x1_0000_0800UL, JsonObjectHeap.GlobalNullAccessCallbackContext);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DispatchValueConstructor_RunsHandlerAndReturnsThis()
|
||||
{
|
||||
JsonObjectHeap.ResetForTests();
|
||||
var manager = CreateRegisteredManager();
|
||||
var ctx = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
|
||||
ctx[CpuRegister.Rdi] = 0x1_0000_0000;
|
||||
|
||||
Assert.True(manager.TryDispatch("qBMjqyBn3OM", ctx, out var result));
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||
Assert.Equal(0x1_0000_0000UL, ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[0x1_0000_0000].Kind);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Json;
|
||||
|
||||
// JsonObjectHeap is shared static state; both Json test classes join one collection so xUnit
|
||||
// does not run them in parallel against it.
|
||||
[Collection("JsonObjectHeap")]
|
||||
public sealed class JsonValueExportsTests
|
||||
{
|
||||
private const ulong ThisAddress = 0x1_0000_0000;
|
||||
private const ulong StringAddress = 0x1_0000_1000;
|
||||
private const ulong TextAddress = 0x1_0000_2000;
|
||||
|
||||
private readonly FakeCpuMemory _memory = new(0x1_0000_0000, 0x10000);
|
||||
private readonly CpuContext _ctx;
|
||||
|
||||
public JsonValueExportsTests()
|
||||
{
|
||||
JsonObjectHeap.ResetForTests();
|
||||
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueDefaultConstructor_RegistersNullAndReturnsThis()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
|
||||
JsonValueExports.ValueDefaultConstructor(_ctx);
|
||||
|
||||
Assert.Equal(ThisAddress, _ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[ThisAddress].Kind);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0UL, false)]
|
||||
[InlineData(1UL, true)]
|
||||
[InlineData(0xFFFF_FF00UL, false)] // only the low byte is the bool; 0x00 low byte => false
|
||||
public void ValueSetBoolean_StoresLowByte(ulong raw, bool expected)
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = raw;
|
||||
|
||||
JsonValueExports.ValueSetBoolean(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.Boolean, state.Kind);
|
||||
Assert.Equal(expected, state.Boolean);
|
||||
Assert.Equal(ThisAddress, _ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetInteger_RoundTripsSignedValue()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = unchecked((ulong)-42L);
|
||||
|
||||
JsonValueExports.ValueSetInteger(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.Integer, state.Kind);
|
||||
Assert.Equal(-42L, state.Integer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetUnsigned_RoundTripsFullWidth()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = ulong.MaxValue;
|
||||
|
||||
JsonValueExports.ValueSetUnsigned(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.UInteger, state.Kind);
|
||||
Assert.Equal(ulong.MaxValue, state.UnsignedInteger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetReal_ReadsDoubleFromXmm0()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx.SetXmmRegister(0, unchecked((ulong)BitConverter.DoubleToInt64Bits(3.14159)), 0);
|
||||
|
||||
JsonValueExports.ValueSetReal(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.Real, state.Kind);
|
||||
Assert.Equal(3.14159, state.Real, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetCString_ReadsGuestString()
|
||||
{
|
||||
_memory.WriteCString(TextAddress, "hello json");
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = TextAddress;
|
||||
|
||||
JsonValueExports.ValueSetCString(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||
Assert.Equal("hello json", state.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetType_KeepsRawGuestEnumValue()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = 7;
|
||||
|
||||
JsonValueExports.ValueSetType(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.ExplicitType, state.Kind);
|
||||
Assert.Equal(7u, state.ExplicitType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StringConstructThenValueSetString_CopiesText()
|
||||
{
|
||||
_memory.WriteCString(TextAddress, "from string object");
|
||||
_ctx[CpuRegister.Rdi] = StringAddress;
|
||||
_ctx[CpuRegister.Rsi] = TextAddress;
|
||||
JsonValueExports.StringCStringConstructor(_ctx);
|
||||
|
||||
Assert.Equal("from string object", JsonObjectHeap.Strings[StringAddress]);
|
||||
Assert.Equal(StringAddress, _ctx[CpuRegister.Rax]);
|
||||
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = StringAddress;
|
||||
JsonValueExports.ValueSetString(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||
Assert.Equal("from string object", state.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetString_MissingStringShadow_DegradesToEmpty()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = StringAddress; // never constructed
|
||||
|
||||
JsonValueExports.ValueSetString(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||
Assert.Equal(string.Empty, state.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Destructors_RemoveShadowState()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
JsonValueExports.ValueDefaultConstructor(_ctx);
|
||||
_ctx[CpuRegister.Rdi] = StringAddress;
|
||||
JsonValueExports.StringDefaultConstructor(_ctx);
|
||||
|
||||
Assert.True(JsonObjectHeap.Values.ContainsKey(ThisAddress));
|
||||
Assert.True(JsonObjectHeap.Strings.ContainsKey(StringAddress));
|
||||
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
JsonValueExports.ValueDestructor(_ctx);
|
||||
_ctx[CpuRegister.Rdi] = StringAddress;
|
||||
JsonValueExports.StringDestructor(_ctx);
|
||||
|
||||
Assert.False(JsonObjectHeap.Values.ContainsKey(ThisAddress));
|
||||
Assert.False(JsonObjectHeap.Strings.ContainsKey(StringAddress));
|
||||
Assert.Equal(0UL, _ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueSetCString_FaultingPointer_DegradesToEmptyString()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = ThisAddress;
|
||||
_ctx[CpuRegister.Rsi] = 0xDEAD_0000_0000; // outside the mapped region
|
||||
|
||||
JsonValueExports.ValueSetCString(_ctx);
|
||||
|
||||
var state = JsonObjectHeap.Values[ThisAddress];
|
||||
Assert.Equal(JsonValueKind.String, state.Kind);
|
||||
Assert.Equal(string.Empty, state.Text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
||||
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Microsoft.NET.Test.Sdk": {
|
||||
"type": "Direct",
|
||||
"requested": "[17.14.1, )",
|
||||
"resolved": "17.14.1",
|
||||
"contentHash": "HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeCoverage": "17.14.1",
|
||||
"Microsoft.TestPlatform.TestHost": "17.14.1"
|
||||
}
|
||||
},
|
||||
"xunit": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.9.3, )",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==",
|
||||
"dependencies": {
|
||||
"xunit.analyzers": "1.18.0",
|
||||
"xunit.assert": "2.9.3",
|
||||
"xunit.core": "[2.9.3]"
|
||||
}
|
||||
},
|
||||
"xunit.runner.visualstudio": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.1, )",
|
||||
"resolved": "3.1.1",
|
||||
"contentHash": "gNu2zhnuwjq5vQlU4S7yK/lfaKZDLmtcu+vTjnhfTlMAUYn+Hmgu8IIX0UCwWepYkk+Szx03DHx1bDnc9Fd+9w=="
|
||||
},
|
||||
"Microsoft.CodeCoverage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.14.1",
|
||||
"contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg=="
|
||||
},
|
||||
"Microsoft.DotNet.PlatformAbstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.9",
|
||||
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
|
||||
},
|
||||
"Microsoft.TestPlatform.ObjectModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.14.1",
|
||||
"contentHash": "xTP1W6Mi6SWmuxd3a+jj9G9UoC850WGwZUps1Wah9r1ZxgXhdJfj1QqDLJkFjHDCvN42qDL2Ps5KjQYWUU0zcQ=="
|
||||
},
|
||||
"Microsoft.TestPlatform.TestHost": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.14.1",
|
||||
"contentHash": "d78LPzGKkJwsJXAQwsbJJ7LE7D1wB+rAyhHHAaODF+RDSQ0NgMjDFkSA1Djw18VrxO76GlKAjRUhl+H8NL8Z+Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.TestPlatform.ObjectModel": "17.14.1",
|
||||
"Newtonsoft.Json": "13.0.3"
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.0.3",
|
||||
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
||||
},
|
||||
"Silk.NET.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||
"Microsoft.Extensions.DependencyModel": "9.0.9"
|
||||
}
|
||||
},
|
||||
"Silk.NET.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Ultz.Native.GLFW": "3.4.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Maths": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
|
||||
},
|
||||
"Silk.NET.Windowing.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Maths": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
|
||||
"dependencies": {
|
||||
"Silk.NET.GLFW": "2.23.0",
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"xunit.abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
|
||||
},
|
||||
"xunit.analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.18.0",
|
||||
"contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ=="
|
||||
},
|
||||
"xunit.assert": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
|
||||
},
|
||||
"xunit.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==",
|
||||
"dependencies": {
|
||||
"xunit.extensibility.core": "[2.9.3]",
|
||||
"xunit.extensibility.execution": "[2.9.3]"
|
||||
}
|
||||
},
|
||||
"xunit.extensibility.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==",
|
||||
"dependencies": {
|
||||
"xunit.abstractions": "2.0.3"
|
||||
}
|
||||
},
|
||||
"xunit.extensibility.execution": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==",
|
||||
"dependencies": {
|
||||
"xunit.extensibility.core": "[2.9.3]"
|
||||
}
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user