Skip to content

Replace C# parser stack with Rust FFI via redguard-preservation#61

Open
michidk wants to merge 21 commits intoRGUnity:masterfrom
michidk:feat/ffi-benchmark
Open

Replace C# parser stack with Rust FFI via redguard-preservation#61
michidk wants to merge 21 commits intoRGUnity:masterfrom
michidk:feat/ffi-benchmark

Conversation

@michidk
Copy link
Copy Markdown
Member

@michidk michidk commented Mar 23, 2026

Summary

Replaces the custom C# game file parsing pipeline with Rust FFI calls to redguard-preservation. Scene loading, model parsing, texture decoding, audio conversion, and subtitle loading all go through the native library. The entire legacy C# parser stack (~5900 lines) has been removed.

Performance

Full Island Scene Load (Stros M'Kai)

C# Parser (master) Rust FFI (this PR)
Total time 3,339 ms 848 ms
Meshes 3,000 3,000
Vertices 984,609 984,609
Triangles 328,587 328,587
Speedup 3.9×

Geometry output matches exactly. The FFI path also creates 556 point lights and flat sprite quads that the C# path previously skipped.

Changes

  • Removed 15 legacy C# file parsers and 6 store classes (~5900 lines), replaced by a new Assets/Scripts/FFI/ layer with P/Invoke bindings, zero-copy deserializers, and cached loaders for models, textures, audio (SFX + RTX), loading screens, and world data
  • Added Assets/Editor/RgprePluginDownloader.cs — editor tool to download rgpre binaries for all platforms from GitHub releases
  • Added Assets/Plugins/rgpre/ — native plugin binaries organized by platform (Windows/x86_64/, Linux/x86_64/, macOS/arm64/, macOS/x86_64/)
  • Updated WorldLoader, ModelViewer, GLTFExporter, and RGScriptedObject to route through the FFI adapters
  • Kept gameplay runtime scripts (RGScriptedObject, RGObjectStore, RGRGMAnimStore, RGRGMScriptStore) — now populated from FFI data sources

Architecture

Game Files (.3D, .ROB, .RGM, .WLD, .TEXBSI, .GXA, .SFX, .RTX)
    ↓ file paths
rgpre.dll (Rust — stateless, path-based, internal texture cache)
    ↓ RGMD binary / RGBA pixels / WAV bytes / subtitle text
C# Adapters (FFIModelLoader, FFITextureLoader, FFISoundStore, ...)
    ↓ Mesh / Texture2D / Material / AudioClip
Unity Scene

FFI boundary — what crosses and what doesn't

The FFI layer (rgpre) handles all complex proprietary binary format parsing. The C# side only directly reads trivial/flat formats:

Access method File types Why
Through FFI (rgpre) .RGM sections, .3D/.3DC/.ROB models, .WLD terrain, TEXBSI textures, .SFX/.RTX audio, .GXA animations, .FNT fonts Complex proprietary binary container formats requiring structured parsing
Direct C# file I/O WORLD.INI (text INI), .COL palettes (raw RGB bytes), config/save JSON Trivial flat formats — no binary container logic needed

What the FFI currently covers

Domain FFI functions Output format
Meshes rg_parse_model_data, rg_parse_rob_data, rg_parse_wld_terrain_data Structured (RGMD — submesh headers + vertices + indices)
Scene rg_parse_rgm_placements Structured (RGPL — placements + lights with transforms)
Textures rg_decode_texture, rg_decode_texture_all_frames, rg_texbsi_image_count, rg_decode_gxa, rg_gxa_frame_count Structured (header + RGBA pixels)
Audio rg_convert_sfx_to_wav, rg_sfx_effect_count, rg_convert_rtx_entry_to_wav, rg_rtx_entry_count, rg_get_rtx_subtitle WAV bytes / UTF-8 subtitle text
Fonts rg_convert_fnt_to_ttf TTF bytes
GLB export rg_convert_model_from_path, rg_convert_rgm_from_path, rg_convert_wld_from_path Binary GLB
RGM sections rg_rgm_section_count, rg_get_rgm_section Raw section bytes

What's not yet covered — remaining C# parsing

The gameplay runtime scripts kept in this PR still do their own binary parsing from raw RGM section bytes. The Rust side already has parsers for all of this data — future FFI additions would let the C# side consume structured data instead of re-parsing the binary.

Data What Rust parses today What C# still parses C# file
SOUP scripts ActorScript — per-actor bytecode slice, resolved string table, variable table, script PC/offset/length from RAHD+RASC+RAST+RASB+RAVA Full binary parsing of RAHD records, RASC bytecode extraction, RAST/RASB string resolution, RAVA variable loading RGRGMScriptStore.cs (~55 KB)
SOUP386.DEF SoupDef — 367 function names + param counts, flags, references, attributes, animation group names Hardcoded function ID→name table and implementations soupdeffcn_nimpl.cs (~79 KB)
RAGR animation groups RagrAnimGroup — group index, anim ID, flag, frame count, command list Binary parsing of RAGR section, group/command structures RGRGMAnimStore.cs (~22 KB)
RAEX combat data RaexRecord — grip points, scabbard slots, range min/ideal/max, taunt ID, texture ID Binary parsing of RAEX records Inline in RGM loading
RAVC collision volumes RavcRecord — offset XYZ, vertex index, radius Binary parsing of RAVC records Inline in RGM loading
RTX label map Script 4-char key → audio filename mapping Not currently used — would connect SOUP dialogue calls to RTX audio files

Note: rg_get_rgm_section exists as an escape hatch to get raw section bytes for any RGM tag. The C# runtime scripts use this today. The goal for future FFI work is to replace those raw-byte consumers with structured data, so parsing lives in one place (Rust) and C# only runs the runtime/interpreter.

DLL Version

rgpre v0.5.1 — source: redguard-preservation

Integrate redguard-preservation Rust library via P/Invoke for native
parsing of Redguard game assets. Includes complete bindings for all 21
FFI functions covering models, textures, audio, and scene conversion.

New files:
- Assets/Plugins/rgpre.dll — prebuilt Rust cdylib (v0.1.0)
- Assets/Scripts/FFI/RgpreBindings.cs — P/Invoke bindings (21 functions)
- Assets/Scripts/FFI/RgmdDeserializer.cs — RGMD binary → Unity Mesh
- Assets/Editor/RgprePluginDownloader.cs — auto-download native plugin
  from GitHub releases for any platform (Win/Mac/Linux)

Also adds GLTFExporter.ExportToPath() for headless GLB export.
@michidk michidk force-pushed the feat/ffi-benchmark branch from 907a1bb to f23e8fd Compare March 23, 2026 10:22
@michidk michidk changed the title Add Rust FFI integration and benchmarks for scene loading use redguard-preservation project for redguard parsing Mar 23, 2026
michidk and others added 12 commits March 23, 2026 11:38
Rewrite GLTFExporter to use RgpreBindings instead of GLTFast. Export
now goes directly from raw game files to GLB via the Rust library,
bypassing Unity's scene serialization entirely.

- Delete Export.asmdef (no longer needed without GLTFast)
- Remove com.unity.cloud.gltfast package dependency
- GLTFExporter reads raw files and calls rg_convert_*_to_glb
- ModelViewer sets export context (model/area, file names, palette)
- Support single model, ROB, RGM, and WLD+RGM area export
SpawnModel now uses FFIModelLoader instead of the C# parsing pipeline:
- FFIModelLoader.Load3D/Load3DC/LoadROB call rg_parse_model_data and
  rg_parse_rob_data via P/Invoke, deserialize RGMD binary into Unity
  Meshes, and create GameObjects with proper materials
- FFITextureLoader decodes textures via rg_decode_texture FFI, with
  caching for both textures and palette colors
- RgmdDeserializer extended to return per-submesh material info
  (solid color index or texture_id + image_id) and frame count

SpawnArea still uses the old C# ModelLoader.LoadArea (blocked on
upstream rg_parse_wld_terrain_data + RGM placement FFI).
MCP for Unity is a dev-only tool, not a project dependency.
Newtonsoft.Json was a transitive dep from MCP. Neither belongs in the PR.
Only change vs master: removal of com.unity.cloud.gltfast.
SpawnArea now uses FFIModelLoader.LoadArea which goes entirely through
the Rust FFI:
- rg_parse_rob_data → mesh dictionary (unique models)
- rg_parse_rgm_placements → RGPL binary (positions, rotations, lights)
- rg_parse_wld_terrain_data → RGMD terrain mesh
- rg_decode_texture → texture decoding per submesh

New files:
- RgplDeserializer.cs — parses RGPL binary (placements + lights)

Updated:
- RgpreBindings.cs — added rg_parse_wld_terrain_data, rg_parse_rgm_placements
- FFIModelLoader.cs — added LoadArea with full scene assembly
- ModelViewer.cs — SpawnArea uses FFIModelLoader
- PlayerMeshLoader.cs — uses FFIModelLoader.Load3DC
- ModelLoader.cs — removed dead Load3D/Load3DC/LoadROB methods

Updated rgpre.dll to v0.2.2 with terrain and placement FFI support.
…sprites

- RgpreBindings: rg_decode_texture now takes TextureCache handle instead
  of raw TEXBSI+palette bytes (matches upstream API change)
- FFITextureLoader: creates native TextureCache once, reuses for all
  decode calls (eliminates redundant disk I/O per texture)
- RgplDeserializer: reads texture_id + image_id from placement records
- FFIModelLoader: handles flat sprite placements as textured quads
- Lookup uses modelName directly (upstream now strips extensions)

Benchmark: 3000 meshes, 984K verts, 328K tris — exact geometry match
- SceneLoader: replace RGTexStore.MaterialDict with renderer iteration,
  replace RGScriptedObject animation with BlendShapeAnimator
- PlayerMeshLoader: replace RGScriptedObject animation with BlendShapeAnimator
- Both now fully use FFI path with zero old parsing dependencies
- Rewrite FFI layer to path-based API (DLL handles all I/O)

- Delete entire RGFileImport parser stack (RGGFXImport/, stores, ModelLoader)

- Keep runtime code (RGScriptedObject, AnimStore, ScriptStore) with FFI data sources

- Add FFIWorldStore (C# INI parser), FFIGxaLoader, FFISoundStore adapters

- Add RGM section access via rg_get_rgm_section for anim/script/attribute data

- C#-side UV normalization (material texture scale) since DLL returns /16.0 UVs

- Global mesh, ROB, and material caching across LoadArea calls

- Performance timing instrumentation in LoadArea

- Update to rgpre v0.3.3 (texture cache, stateless model parse, RGM sections)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- Replace BinaryReader/MemoryStream with direct pointer walking

- Add NativeBuffer, ReadBuffer/FreeBuffer pattern for deferred free

- Add repr(C) struct definitions: TextureHeader, RgmdHeader, RgmdSubmeshHeader, RgmdVertex, RobHeader, RobSegmentHeader, RgplHeader, RgplPlacement, RgplLight

- RGMD submesh header now fixed 16 bytes, ROB segment header always 16 bytes

- RGPL placement field order: textureId, imageId, objectType

- Update to rgpre v0.3.4

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…remove dead code, split long methods

- Fix P0 bug: TryGetMeshData always returned false due to duplicate return block
- Extract FFIPathUtils with shared ResolveFile() and NormalizeModelName()
- Move EnsureReadable() to RgpreBindings as shared utility
- Extract EnsureRobCached() to deduplicate ROB parse-and-cache logic
- Remove ~150 lines dead code (BuildRuntimeObjectsFromMetadata, unused vars, dead types)
- Remove unreachable LoadRTX body and dead GetRtxMetadataByIndex in FFISoundStore
- Split LoadArea (177 lines) into PlaceRgplObjects, CreateLights, LoadTerrain
- Split LoadRgmSections (201 lines) into PopulateRawSections, ParseRAHD, ParseMPOB
- Fix naming: worldData→WorldData, scriptedObjects→ScriptedObjects, SFXList→SfxClips
- Add Destroy() calls in ClearCache to prevent GPU resource leaks
- Add StringComparer.OrdinalIgnoreCase to materialCache for consistency
- Refactor FFIGxaLoader to use RgpreBindings.ReadBuffer + TextureHeader struct
- Extract magic numbers to named constants (WavMinHeaderSize, Pcm scales, RalcItemSize)
@michidk michidk changed the title use redguard-preservation project for redguard parsing Replace C# parser stack with Rust FFI via redguard-preservation Mar 25, 2026
michidk added 7 commits March 25, 2026 20:10
…cker

- Download all 4 platform binaries (Win64, macOS x64, macOS ARM64, Linux x64) per release
- Place in platform-specific subfolders to avoid filename conflicts (macOS archs)
- Add 'Tools/rgpre/Download Specific Version...' menu with EditorWindow prompt
- Rename menu parent to 'Tools/rgpre/' with 'Update to Latest' and version picker
- Configure PluginImporter with correct CPU and OS per platform
- Support both latest and tag-based GitHub release API endpoints
… DLL

- All platform binaries now live under Assets/Plugins/rgpre/{OS}/{arch}/
- Remove old Assets/Plugins/rgpre.dll and version.txt that caused name conflicts
…X loaded flags

Replace RTX loading stub with real rgpre-based decoding (ConvertRtxEntryToWav),
matching the existing SFX loading pattern. Add AmbientRtx script function (96)
mirroring AmbientSound but pulling from RTX store. Fix SFXLoaded/RTXLoaded
flags that were never set to true, causing redundant reloads on every world
transition.
…d RTX subtitle binding

Add missing assets_dir parameter to ParseModelData and ParseRobData to match
the updated rgpre native API (palette resolution for solid-color materials).
Add rg_get_rtx_subtitle binding and wire it into LoadRTX so entries load
with both audio and subtitle text.
Use rg_decode_texture_all_frames to load every frame for animated textures
(water, fire, torches) instead of only frame 0. All frames are stored on
the material via SetTexture(FRAME_{n}), matching the old C# parser behavior
and preparing for a future runtime texture animation system.
…d RGB for solid colors, fix stale material cache on reload
@michidk michidk marked this pull request as ready for review March 29, 2026 17:12
# Conflicts:
#	Assets/Scripts/RGFileImport/RGMData/RGScriptedObject.cs
#	Assets/Scripts/RGFileImport/RGMeshStore.cs
@michidk michidk requested review from Thane5 and culacant March 29, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant