From df53ff59d9dde8ee829f34b939f4f5010947af5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Caravaca=20Carretero?= <114653514+Chelu97@users.noreply.github.com> Date: Wed, 15 Jul 2026 00:49:33 +0200 Subject: [PATCH] [Json] Implement sce::Json::Value and String (construct / set / destroy) (#169) * [Json] Implement sce::Json::Value and Json::String construct/set/destroy libSceJson previously only had the Initializer/MemAllocator setup path. The Value and String classes themselves were entirely absent, so a Prospero title that builds a JSON tree (Quake PPSA01880 does, to shape a web-API request) hit unresolved imports and faulted on the call. The imports it left unresolved right before its access violation are exactly these Value ctors/setters and String ctor/dtor. Model the Value/String payload host-side (JsonObjectHeap), keyed by the guest `this` pointer, following the handle-shadow pattern already used by Ngs2Exports. The guest object bytes are deliberately not written: these objects are usually stack-allocated with an unknown real layout, and writing a guessed layout risks smashing an adjacent stack canary (the same hazard the AudioOut2 context-param note in this tree records). Constructors and setters follow the Itanium ABI and return `this` in rax, which is correct whether the real setter returns void or Value&. Covered NIDs (complete-object C1/D1 variants, matching the observed imports): Value(default/bool/long/ulong/double/ValueType/char*/String), Value::~Value, Value::set(bool/long/ulong/double/ValueType/char*/String), Value::clear, String(char*/default/copy), String::~String. Only the payload the guest can reach through library methods is modelled; direct guest reads of the object bytes are out of scope and would need observed layout evidence. Co-Authored-By: Claude Fable 5 * [Tests] Add SharpEmu.Libs.Tests covering the Json Value/String exports First test project for SharpEmu.Libs (xunit), the SharpEmu.Libs.Tests layout the maintainer already agreed to in issue #36. - A FakeCpuMemory (single contiguous region) drives the exports at the CpuContext level with no live guest. - Direct-call tests: ctor/setter round-trips for bool/int/uint/double (read from xmm0)/char*/String/ValueType, destructor cleanup, and the graceful-degradation paths (missing String shadow and a faulting char* pointer both fall back to the empty string instead of throwing). - Registration test: a real ModuleManager scans SharpEmu.Libs and the nine NIDs Quake left unresolved now resolve to the libSceJson exports and dispatch cleanly (returns `this` in rax). InternalsVisibleTo exposes JsonObjectHeap to the test assembly. The test project's packages.lock.json is committed for CI locked-mode restore; CI does not run tests yet, left as a maintainer decision. Co-Authored-By: Claude Fable 5 * [Json] Add Initializer::setGlobalNullAccessCallback Quake calls it during kexPSNWebAPI::Initialize and treats the not-found error as fatal for the whole Np Web API bring-up. Store the guest hook (never invoked by this HLE: shadows degrade to defaults instead of dereferencing missing members) and return success. Verified against the dump: the "setGlobalNullAccessCallback failed (0x80020002)" line is gone and kexPSNWebAPI::Initialize now logs "Np Web API Initialized"; the next blockers are sceNpAuthCreateRequest and sceUserServiceInitialize ordering, outside libSceJson. Also pins both Json test classes to one xunit collection: they share JsonObjectHeap statics and parallel class execution raced ResetForTests against a running test. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- Directory.Packages.props | 3 + SharpEmu.slnx | 3 + src/SharpEmu.Libs/Json/JsonExports.cs | 24 ++ src/SharpEmu.Libs/Json/JsonValueExports.cs | 234 ++++++++++++++++++ src/SharpEmu.Libs/Json/JsonValueModel.cs | 110 ++++++++ src/SharpEmu.Libs/SharpEmu.Libs.csproj | 4 + tests/SharpEmu.Libs.Tests/FakeCpuMemory.cs | 68 +++++ .../Json/JsonExportRegistrationTests.cs | 80 ++++++ .../Json/JsonValueExportsTests.cs | 188 ++++++++++++++ .../SharpEmu.Libs.Tests.csproj | 22 ++ tests/SharpEmu.Libs.Tests/packages.lock.json | 212 ++++++++++++++++ 11 files changed, 948 insertions(+) create mode 100644 src/SharpEmu.Libs/Json/JsonValueExports.cs create mode 100644 src/SharpEmu.Libs/Json/JsonValueModel.cs create mode 100644 tests/SharpEmu.Libs.Tests/FakeCpuMemory.cs create mode 100644 tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Json/JsonValueExportsTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj create mode 100644 tests/SharpEmu.Libs.Tests/packages.lock.json diff --git a/Directory.Packages.props b/Directory.Packages.props index b795f3c..4a05189 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,11 +12,14 @@ SPDX-License-Identifier: GPL-2.0-or-later + + + \ No newline at end of file diff --git a/SharpEmu.slnx b/SharpEmu.slnx index 3c45742..045ee20 100644 --- a/SharpEmu.slnx +++ b/SharpEmu.slnx @@ -12,4 +12,7 @@ SPDX-License-Identifier: GPL-2.0-or-later + + + diff --git a/src/SharpEmu.Libs/Json/JsonExports.cs b/src/SharpEmu.Libs/Json/JsonExports.cs index 78c65d5..c62262b 100644 --- a/src/SharpEmu.Libs/Json/JsonExports.cs +++ b/src/SharpEmu.Libs/Json/JsonExports.cs @@ -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", diff --git a/src/SharpEmu.Libs/Json/JsonValueExports.cs b/src/SharpEmu.Libs/Json/JsonValueExports.cs new file mode 100644 index 0000000..f781a24 --- /dev/null +++ b/src/SharpEmu.Libs/Json/JsonValueExports.cs @@ -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); + } +} diff --git a/src/SharpEmu.Libs/Json/JsonValueModel.cs b/src/SharpEmu.Libs/Json/JsonValueModel.cs new file mode 100644 index 0000000..b5415d8 --- /dev/null +++ b/src/SharpEmu.Libs/Json/JsonValueModel.cs @@ -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 Values { get; } = new(); + + public static ConcurrentDictionary 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; + } +} diff --git a/src/SharpEmu.Libs/SharpEmu.Libs.csproj b/src/SharpEmu.Libs/SharpEmu.Libs.csproj index df6de17..4d19ca1 100644 --- a/src/SharpEmu.Libs/SharpEmu.Libs.csproj +++ b/src/SharpEmu.Libs/SharpEmu.Libs.csproj @@ -8,6 +8,10 @@ SPDX-License-Identifier: GPL-2.0-or-later + + + + diff --git a/tests/SharpEmu.Libs.Tests/FakeCpuMemory.cs b/tests/SharpEmu.Libs.Tests/FakeCpuMemory.cs new file mode 100644 index 0000000..defe7f7 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/FakeCpuMemory.cs @@ -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 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 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; + } +} diff --git a/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs b/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs new file mode 100644 index 0000000..a6cb064 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Json/JsonExportRegistrationTests.cs @@ -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); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Json/JsonValueExportsTests.cs b/tests/SharpEmu.Libs.Tests/Json/JsonValueExportsTests.cs new file mode 100644 index 0000000..77b7225 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Json/JsonValueExportsTests.cs @@ -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); + } +} diff --git a/tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj b/tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj new file mode 100644 index 0000000..f96d936 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj @@ -0,0 +1,22 @@ + + + + + false + false + + + + + + + + + + + + + diff --git a/tests/SharpEmu.Libs.Tests/packages.lock.json b/tests/SharpEmu.Libs.Tests/packages.lock.json new file mode 100644 index 0000000..879d342 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/packages.lock.json @@ -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" + } + } + } + } +} \ No newline at end of file