Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 38 additions & 31 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,51 +1,58 @@
name: tests

on:
push:
branches: [master, main]
pull_request:
workflow_dispatch:

env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true

jobs:
test:
name: build and test
name: JIT (NodeData=${{ matrix.nodedata }})
runs-on: ubuntu-latest
timeout-minutes: 20

strategy:
matrix:
nodedata: [false, true]
steps:
- uses: actions/checkout@v4

# The test project targets net10.0 and the libraries multi-target down to netstandard2.1, so the newest SDK in the
# matrix is the one that has to be present.
- name: Set up .NET
uses: actions/setup-dotnet@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 11.0.x

# Restore and build separately so a compile error is reported before any test host is started.
- name: Restore
run: dotnet restore UnityAsset.NET.sln

- name: Build
run: dotnet build UnityAsset.NET.sln --no-restore --configuration Release

dotnet-version: |
10.0.x
11.0.x
- name: Build libraries and tests
run: dotnet build tests/UnityAsset.NET.Tests -c Release -p:UnityAssetNodeData=${{ matrix.nodedata }}
- name: Test
run: >
dotnet test tests/UnityAsset.NET.Tests/UnityAsset.NET.Tests.csproj
--no-build
--configuration Release
--logger "trx;LogFileName=results.trx"
--results-directory TestResults

- name: Upload test results
# Always: a failed run is exactly when the report is worth having.
run: dotnet test tests/UnityAsset.NET.Tests -c Release --no-build --logger "trx;LogFileName=results.trx" --results-directory TestResults
- name: Smoke and synthetic benchmark
run: dotnet run --project tests/UnityAsset.NET.AotSmoke -c Release -p:UnityAssetNodeData=${{ matrix.nodedata }} -- --benchmark
- name: Build interface generator
if: matrix.nodedata == false
run: dotnet build UnityAsset.NET.UnityTypeGen -c Release
- uses: actions/upload-artifact@v4
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
name: test-results-${{ matrix.nodedata }}
path: TestResults
if-no-files-found: ignore
if-no-files-found: ignore
native-aot:
name: NativeAOT smoke
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: |
10.0.x
11.0.x
- name: Publish (automatically selects NodeData)
run: dotnet publish tests/UnityAsset.NET.AotSmoke -c Release -r linux-x64 -p:PublishAot=true -o artifacts/aot-smoke
- name: Native smoke and synthetic benchmark
run: ./artifacts/aot-smoke/UnityAsset.NET.AotSmoke --benchmark
- uses: actions/upload-artifact@v4
with:
name: native-aot-smoke
path: artifacts/aot-smoke
47 changes: 47 additions & 0 deletions AOT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# AOT and NodeData

Build the library from source with `-p:UnityAssetNodeData=true` to select the NodeData backend on a normal JIT runtime. `-p:PublishAot=true` automatically selects it. The corresponding C# symbol is `UNITYASSET_NODEDATA`, which can also be defined directly when compiling the library. These are library build settings: defining a symbol in an application does not change an already compiled NuGet DLL. Publish the application using a project reference to this source tree, or package a library built with the fallback setting.

The ordinary build keeps runtime code generation. The fallback build excludes the runtime compiler/assembly-loader implementation of `TypeRegistry`. Roslyn remains a build dependency for the interface generator; unreachable compiler helpers are removed by NativeAOT trimming. No runtime interface proxies, reflection-based constructors, or blanket linker roots are used by the fallback backend.

`NodeAdapterGenerator` generates concrete interface implementations and typed conversion functions during compilation. It consumes the checked-in interfaces, including handwritten partial declarations and generic interfaces, so there is no second TPK-dependent adapter generation step. Run the existing UnityTypeGen tool when updating the interface definitions. Unknown root types retain schema-based reading/writing; named roots also implement `INamedObject`.

Handwritten predefined types (such as GameObject, SpriteAtlas and the vector types) retain their existing direct parsers. MonoBehaviour retains its existing NodeData implementation. The new fallback replaces the types that previously needed runtime-generated code. Typed arrays and nested objects are cached and flushed into their schema nodes before writing, so in-place mutations are preserved. Fields absent in a Unity version keep the optional interface behavior; fields absent from the interface remain in the node tree and survive serialization. Unity maps retain ordering and duplicate keys. RefSum alternatives are selected from the schema, including empty and jagged arrays.

## Validation

```powershell
# Add -p:UnityAssetTargetFramework=net10.0 if only the .NET 10 SDK is installed.
dotnet run --project tests/UnityAsset.NET.AotSmoke -c Release -- --benchmark
dotnet run --project tests/UnityAsset.NET.AotSmoke -c Release -p:UnityAssetNodeData=true -- --benchmark
dotnet publish tests/UnityAsset.NET.AotSmoke -c Release -r win-x64 -p:PublishAot=true -o artifacts/aot-smoke
./artifacts/aot-smoke/UnityAsset.NET.AotSmoke.exe --benchmark --tpk C:/path/uncompressed.tpk
```

The smoke executable checks typed reads, setters, optional fields, preservation of unknown fields, duplicate map keys, nested mutation, empty arrays and version-dependent RefSum array ranks, signedness changes, pointer variants, sanitized names and per-pair map alignment. It can run as an ordinary executable or a native binary without a test runner. CI runs the existing unit suite in both JIT configurations and publishes/runs a Linux NativeAOT binary. The interface generator is built separately as a development tool.

## Benchmarks

```powershell
./benchmarks/Compare.ps1 -Tpk C:/path/uncompressed.tpk
# Add a serialized file or bundle for the dataset benchmark:
./benchmarks/Compare.ps1 -Tpk C:/path/uncompressed.tpk -Dataset C:/path/assets.bundle
```

The script publishes three independent Release outputs and runs five fresh processes per mode. Results are saved under `artifacts/benchmarks/{jit,nodedata,aot}/results.txt`. Use `-Runtime linux-x64` on a Linux host with the NativeAOT prerequisites installed. Cross-compiling between operating systems is not supported by this script.

The synthetic workload uses the same TextAsset schema and payload in every mode. It reports cold registry setup plus first parse, time per parsed object after warmup, managed allocations per object, and a checksum. The factory is invoked directly, bypassing Asset.Value caching. The optional dataset path reports load time separately from one parse/write pass over every asset. TPK is a schema database, not a real asset dataset. Synthetic results should not be interpreted as representative game-asset throughput; use multiple process samples and a real dataset before drawing performance conclusions.

Local validation used .NET 10 on Windows x64 and the supplied TPK (366 roots for 2017.1.0b1). Regenerating interfaces with that TPK and minimum version produced no differences. The local workspace also contains an untracked AssetModificationTests.cs from an earlier branch that references a removed API; that file is preserved and excluded only from local unit-test invocations, not from CI.

## Local synthetic results

Windows x64, .NET 10, Release; median of five fresh processes, 10,000 warmup parses and 100,000 measured parses per process. These are TextAsset microbenchmarks, not real-dataset results. Timing includes parsing and reading two string properties; the cold column includes registry setup and the first parse.

| Mode | Cold setup + first parse (ms) | Parse + access (ns/object) | Managed bytes/object |
| --- | ---: | ---: | ---: |
| jit | 1046.909 | 585.9 | 472 |
| nodedata | 20.810 | 910.2 | 1272 |
| aot | 0.189 | 444.1 | 1168 |

Final local checks: 565/565 unit tests passed in each JIT configuration. All three Release smoke executables passed, including NativeAOT with dynamic code disabled and TPK loading. The final publish emitted no IL trimming/AOT warnings. .NET 11 and Linux CI jobs are configured but were not executed locally.
3 changes: 3 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<Project>
<PropertyGroup>
<UnityAssetNodeData Condition="'$(PublishAot)' == 'true'">true</UnityAssetNodeData>
</PropertyGroup>
<PropertyGroup>
<Authors>AXiX</Authors>
<Copyright>AXiX</Copyright>
Expand Down
10 changes: 10 additions & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project>
<PropertyGroup>
<UnityAssetNodeData Condition="'$(PublishAot)' == 'true'">true</UnityAssetNodeData>
<DefineConstants Condition="'$(UnityAssetNodeData)' == 'true' or '$(PublishAot)' == 'true'">$(DefineConstants);UNITYASSET_NODEDATA</DefineConstants>
</PropertyGroup>
<!-- Allows validating the library with a single installed SDK; leaves analyzer targets intact. -->
<PropertyGroup Condition="'$(UnityAssetTargetFramework)' != '' and '$(TargetFrameworks)' != ''">
<TargetFrameworks>$(UnityAssetTargetFramework)</TargetFrameworks>
</PropertyGroup>
</Project>
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,6 @@ This project stands on the shoulders of these amazing open-source projects:
- [AssetStudio](https://github.com/aelurum/AssetStudio) by [aelurum](https://github.com/aelurum)
- [AssetsTools.NET](https://github.com/nesrak1/AssetsTools.NET) by [nesrak1](https://github.com/nesrak1)
- [UnityPy](https://github.com/K0lb3/UnityPy) by [K0lb3](https://github.com/K0lb3)
- [RustyAssetBundleEXtractor](https://github.com/UniversalGameExtraction/RustyAssetBundleEXtractor) by [UniversalGameExtraction](https://github.com/UniversalGameExtraction)
- [RustyAssetBundleEXtractor](https://github.com/UniversalGameExtraction/RustyAssetBundleEXtractor) by [UniversalGameExtraction](https://github.com/UniversalGameExtraction)

See [AOT and NodeData](AOT.md) for NativeAOT builds, fallback configuration, tests and benchmarks.
3 changes: 3 additions & 0 deletions UnityAsset.NET.TypeTreeHelper/Compiler/Helper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.CodeAnalysis.CSharp;
using System.Reflection;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Microsoft.CodeAnalysis.CSharp.Syntax;

Expand Down Expand Up @@ -225,6 +226,7 @@ public static bool IsNamedAsset(TypeTreeRepr current)

private const string NamedObjectInterfaceName = "INamedObject";

[RequiresUnreferencedCode("Reflection-based schema inspection is a build-time operation; use InterfaceSchemaTable at runtime.")]
public static bool DerivesFromNamedObject(Type? interfaceType)
=> interfaceType is not null
&& (interfaceType.Name == NamedObjectInterfaceName
Expand Down Expand Up @@ -402,6 +404,7 @@ public static bool IsNullable(Type csharpType)
return csharpType.IsGenericType && csharpType.GetGenericTypeDefinition() == typeof(Nullable<>);
}

[RequiresUnreferencedCode("Reflection-based schema inspection requires the complete interface hierarchy. Use InterfaceSchemaTable at runtime.")]
public static Dictionary<string, PropertyInfo> GetAllInterfaceProperties(Type type)
{
var properties = new Dictionary<string, PropertyInfo>();
Expand Down
Loading