Skip to content

C#: the FileEntity hierarchy, copy constructors — and the honest answer on generic entities - #5

Merged
skarndev merged 4 commits into
mainfrom
feature/csharp-entity-hierarchy
Aug 22, 2026
Merged

skarndev merged 4 commits into
mainfrom
feature/csharp-entity-hierarchy

Conversation

@skarndev

Copy link
Copy Markdown
Owner

The second consumer-feedback round, one question at a time. Rides skarndev/welder-csharp#3 (pinned at 772e6c9) — merge that first.

1. ADT<ClientVersion> generics — assessed, and declined

C# cannot faithfully mimic the C++ value-templated interface: generics take types, not values (no non-type template parameters), and one generic definition carries one member set where ADT<V> instantiations genuinely differ per range (version-gated members, retyped members). Faking it with 11 era marker types over the 5 real layouts would also split type identity — ADT<Mop> ≠ ADT<Legion> despite being one native class — breaking is-checks, equality, and the family dispatch. The hierarchy below delivers the intent (version-agnostic, statically-typed common surface) the way C# actually favors.

2. The hierarchy: Formats.FileEntity

Every file-level entity — the six family bases and BLP — now derives Formats.FileEntity. With welder-csharp's new multi-level family surfaces, the root auto-hoists exactly the contract every child hierarchy binds: Validate() and EnsureValid(), dispatching format-then-version through one FileEntity-typed reference. Read/Write stay on the family bases — ADT's alpha parameter breaks uniformity, and the intersection excludes them automatically rather than by hand list.

The build refused to paper over two real pre-existing gaps, both now fixed at the right layer:

  • Single-range families had no surface at all (Skeleton, WDTOcclusion, WDTParticulates — the rod's old two-child minimum). Fixed upstream; Skeleton.ForVersion(...) results finally carry members.
  • The WDT assembly was the one file entity with no validate()/ensure_valid() in C++. Added: a compound over the main file and every engaged satellite (root/occlusion/lights/fogs/particulates member paths), mirroring WMO's; the validation-contracts docs page picks it up automatically.

3. Copy constructors + Dispose

new ADTWotlk(other) — the BCL's own copy idiom — replaces Clone() everywhere a C++ copy constructor is admitted. Dispose stays and now documents itself as optional deterministic release (the SafeHandle finalizer frees native memory on collection; using makes it prompt) — the C# answer to "proper destructor" is that the destructor already exists.

Also: the BLP class welds as BLP for C# (the dotnet style had coerced it to Blp; Python always spelled BLP) — the last straggler from the round-1 acronym directive.

Verified

Guide gains "One root above the families"; FamilySurfaceTests locks the heterogeneous WMO+BLP Validate loop, the Read exclusion, and the copy-ctor surface. Full native rebuild + dotnet test: 34/34.

🤖 Generated with Claude Code

skarndev and others added 4 commits August 22, 2026 21:10
…date gap

Consumer feedback round 2 (welder-csharp 772e6c9, pin bumped):

1. GENERIC ENTITIES (ADT<ClientVersion>): assessed and declined — C#
   generics take types, not values (no non-type template parameters), and
   one generic definition carries ONE member set where the C++
   instantiations differ per range; 11 era markers over 5 layouts would
   also split type identity (ADT<Mop> != ADT<Legion> despite one native
   class), breaking is-checks and dispatch. The hierarchy below is the
   C#-idiomatic answer.

2. THE HIERARCHY: Formats.FileEntity (C++ formats::FileEntityBase, welded
   "FileEntity") roots every file-level entity — the six family bases AND
   BLP derive it. The rod's family surfaces are MULTI-LEVEL now, so the
   root auto-hoists exactly the contract every child hierarchy binds:
   Validate() and EnsureValid(), dispatched format-then-version through one
   FileEntity-typed reference. fs Read/Write stay family-level — ADT's
   alpha parameter breaks uniformity, and the intersection excludes them
   AUTOMATICALLY rather than by hand list.

   The build surfaced two real pre-existing gaps the intersection refused
   to paper over:
   - single-range families (Skeleton, WDTOcclusion, WDTParticulates) had NO
     family surface at all — the rod's two-child minimum; fixed upstream
     (the mark is the opt-in, not the child count), so Skeleton.ForVersion
     results finally carry members;
   - the WDT assembly was the ONE file entity with no validate()/
     ensure_valid() in C++. It has them now — a compound over the main file
     and every engaged satellite (root/occlusion/lights/fogs/particulates
     member paths), mirroring WMO's; the validation-contracts docs page
     picks it up automatically.

3. COPY CONSTRUCTION replaces Clone(): new ADTWotlk(other) — the BCL's own
   copy idiom — everywhere a C++ copy constructor is admitted. Dispose
   stays and now documents itself as OPTIONAL deterministic release (the
   SafeHandle finalizer frees native memory on collection; using makes it
   prompt).

Also: the BLP class welds as BLP for C# (weld_as(cs) — the dotnet style
coerced the all-caps identifier to Blp; Python always spelled BLP).

Guide gains "One root above the families"; FamilySurfaceTests locks the
heterogeneous WMO+BLP Validate loop, the Read exclusion, and the
copy-ctor surface. dotnet test: 34/34 against the fully rebuilt native.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tex records

The WoWRenderLib profile: ~15% of ParseADT inside McnrEntry.get_Normal /
Vector<ushort>.get_Item / FixedArray<uint>.get_Item — element-wise live
views paying interop per element (~4 P/Invokes + 4 allocations per packed
normal, x37k records per tile).

welder-csharp 7f84760 (pin bumped): every POD record class gains a nested
blittable `Data` struct (explicit layout at the native offsets, fixed
buffers for fixed scalar arrays, nested X.Data for nested records; layout
asserted in the shim), and Vector<T>/FixedArray<T> gain AsSpan<TData>() —
ONE interop crossing reinterprets the whole native buffer as a managed
span, writable, zero allocations. 113 mirrors on our surface, McnrEntry
among them: chunk.Normals.AsSpan<McnrEntry.Data>() replaces ~150-200k
P/Invokes per tile with one. No new native symbols — the accessor rides
the existing element-address thunk, so already-built native libraries keep
working.

Guide gains "Bulk access to per-vertex records (C#)" on the maps page (the
exact profiled case); FamilySurfaceTests locks the McnrEntry span
write-through, the live-view round-trip and the size gate. The tests
csproj allows unsafe (fixed-buffer indexing). dotnet test: 35/35.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User review of the bulk API: the AsSpan<McnrEntry.Data>() type argument is
a C# limitation (no associated types — Vector<T> cannot spell T.Data as a
return type, and the span's element must be the MIRROR, not the wrapper
class), but the generator knows the pairing — welder-csharp ad87e50 emits
per-element AsDataSpan() extensions, so the guide's hot loop reads

    var normals = chunk.Normals.AsDataSpan();

The wowlib surface also caught the multi-extent dedup upstream (C3Vector
arrays at two extents share one FixedArray<C3Vector> type — extensions now
claim per element). Guide + FamilySurfaceTests use the sugar; pin bumped;
dotnet test 35/35.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tree byte-identical to the ad87e50 branch head this previously pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@skarndev
skarndev merged commit 7ed0612 into main Aug 22, 2026
8 checks passed
@skarndev
skarndev deleted the feature/csharp-entity-hierarchy branch August 22, 2026 21:23
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