From 8c539ecf7d52c5f883d0b58063f87833a5c3ecbd Mon Sep 17 00:00:00 2001 From: Sergey Shumakov Date: Sat, 22 Aug 2026 21:10:14 +0300 Subject: [PATCH 1/4] csharp: the FileEntity hierarchy, copy constructors, and the WDT validate gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consumer feedback round 2 (welder-csharp 772e6c9, pin bumped): 1. GENERIC ENTITIES (ADT): 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 != ADT 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 --- .claude/context/csharp-bindings.md | 20 +++++++++ cmake/Dependencies.cmake | 2 +- docs/content/guide/version-agnostic.md | 27 +++++++++++ src/wowlib/formats/adt/adt.hpp | 3 +- src/wowlib/formats/blp/blp.hpp | 7 ++- src/wowlib/formats/common/file_entity.hpp | 37 +++++++++++++++ src/wowlib/formats/m2/m2.hpp | 3 +- src/wowlib/formats/m2/skeleton.hpp | 3 +- src/wowlib/formats/wdl/wdl.hpp | 3 +- src/wowlib/formats/wdt/wdt.hpp | 55 ++++++++++++++++++++++- src/wowlib/formats/wmo/wmo.hpp | 3 +- tests/csharp/FamilySurfaceTests.cs | 25 +++++++++++ 12 files changed, 180 insertions(+), 8 deletions(-) create mode 100644 src/wowlib/formats/common/file_entity.hpp diff --git a/.claude/context/csharp-bindings.md b/.claude/context/csharp-bindings.md index 0d339df..9b4c775 100644 --- a/.claude/context/csharp-bindings.md +++ b/.claude/context/csharp-bindings.md @@ -1,5 +1,25 @@ # C#/.NET bindings (welder-csharp rod) +## 2026-08-22 (evening): entity hierarchy + copy ctors (user feedback round 2) + +- **Q1 (ADT generics): assessed INFEASIBLE, by design of C#** + — no non-type template parameters (generics take types, not values), and + one generic definition has ONE member set while the C++ template + instantiations differ per range; 11 era markers over 5 layouts would also + split type identity (ADT != ADT despite one native layout), + breaking is-checks and the family dispatch. The hierarchy below is the + C#-idiomatic answer. +- **Q2: `Formats.FileEntity`** (C++ formats::FileEntityBase, welded+marked + via the macro): derived by the six family bases AND BLP. welder-csharp + a56fca6 makes family surfaces MULTI-LEVEL (synthesized members join the + base's manifest; families process deepest-first), so FileEntity auto-hoists + exactly the shared contract: Validate()/EnsureValid(). fs Read/Write stay + family-level (ADT's alpha param breaks uniformity — intersection excludes + them automatically; do NOT hand-add them to the root). +- **Q3: `public T(T other)` copy constructors replace Clone()** everywhere + (BCL idiom); Dispose stays but every wrapper's doc now states it is + OPTIONAL deterministic release (SafeHandle finalizer covers collection). + ## 2026-08-22 API polish (feature/csharp-api-polish + welder-csharp feature/generic-containers) Four breaking (pre-1.0) surface changes, all four user-directed: diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index 8280971..3e5b962 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -154,7 +154,7 @@ FetchContent_Declare(welder # — welder core resolves those through name_of, no rod feature involved. FetchContent_Declare(welder_csharp GIT_REPOSITORY https://github.com/skarndev/welder-csharp.git - GIT_TAG 258c5f756d7b54aa125b21615b65bb1eacd8f496) + GIT_TAG 772e6c9cf35b7b7a255d1831bf50584749e7f2d5) # --- stb_dxt (BLP DXT/BC compression; single public-domain header) --- # Pinned to the last commit that touched stb_dxt.h (2021-07-12); the URL_HASH diff --git a/docs/content/guide/version-agnostic.md b/docs/content/guide/version-agnostic.md index d244bc7..29b3af9 100644 --- a/docs/content/guide/version-agnostic.md +++ b/docs/content/guide/version-agnostic.md @@ -233,6 +233,33 @@ type systems keep you honest about the rest: }; ``` +## One root above the families (C#) + +Every file-level entity in C# — the six format family bases and `BLP` — +derives **`Formats.FileEntity`**, which carries the contract they all share +(`Validate`, `EnsureValid`). A mixed batch of loaded files processes +uniformly, no format named: + +```csharp +using WoWLib; + +static void AuditAll(IEnumerable entities) +{ + foreach (var e in entities) + { + using var report = e.Validate(); // dispatches format, then version + // ... + } +} +``` + +Reading and writing stay on the family bases (`Formats.WMO.WMO.Read(...)`, +…): ADT's verbs genuinely take an extra alpha-format argument, so a uniform +root signature would be a lie. Copies are the C# copy-constructor idiom — +`new WMOWotlk(model)` — and `Dispose` is optional deterministic cleanup (the +finalizer releases native memory on collection anyway; `using` just makes it +prompt). + ## Converting between eras `convert` is the bridge from generic reading to targeted writing — read from diff --git a/src/wowlib/formats/adt/adt.hpp b/src/wowlib/formats/adt/adt.hpp index 2b0fc1d..2077316 100644 --- a/src/wowlib/formats/adt/adt.hpp +++ b/src/wowlib/formats/adt/adt.hpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -85,7 +86,7 @@ namespace wowlib::formats::adt Construct the concrete version with ADT.for_version(expansion), then read()/write(); the per-version ADT* classes are subclasses. See https://wowdev.wiki/ADT/v18.)") - ]] ADTBase + ]] ADTBase : FileEntityBase { }; diff --git a/src/wowlib/formats/blp/blp.hpp b/src/wowlib/formats/blp/blp.hpp index 2d588be..355afe2 100644 --- a/src/wowlib/formats/blp/blp.hpp +++ b/src/wowlib/formats/blp/blp.hpp @@ -37,6 +37,7 @@ #include #include #include +#include namespace wowlib::formats::blp { @@ -207,6 +208,10 @@ namespace wowlib::formats::blp struct [[ =welder::weld, + // The dotnet style would coerce the all-caps identifier to Blp; the + // format acronym is the name (round-1 API feedback). Python already + // spells BLP, so the rename is cs-scoped. + =welder::weld_as(wowlib::lang::cs, "BLP"), =welder::doc(R"( A BLP2 texture file — every WoW client release reads the same layout, so the class carries no client-version axis. read()/write() move the @@ -216,7 +221,7 @@ namespace wowlib::formats::blp settings) rebuilds the palette/compression/mip chain from one. Raw payload access goes through mip()/set_mip(). See https://wowdev.wiki/BLP.)") - ]] BLP + ]] BLP : FileEntityBase { [[=welder::doc("The header version field; 1 in every shipped file.")]] std::uint32_t version = blp_version_1; diff --git a/src/wowlib/formats/common/file_entity.hpp b/src/wowlib/formats/common/file_entity.hpp new file mode 100644 index 0000000..b1b5c8b --- /dev/null +++ b/src/wowlib/formats/common/file_entity.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace wowlib::formats +{ + /** The version-agnostic root of every file-level entity (welded as + "FileEntity"). + + Like the per-format `*Base` classes, this empty base exists ENTIRELY for + the language bindings: it gives every file entity — the six versioned + family bases (WMO, M2, Skeleton, ADT, WDT, WDL) and the unversioned BLP — + one common welded supertype, so binding users can hold a heterogeneous + collection and run the contract every entity shares + (`Validate`/`EnsureValid`; the fs `Read`/`Write` pair stays on the family + bases, because ADT's genuinely takes an extra alpha-format argument). + In C# the family-surface synthesis derives that shared contract + automatically — it hoists exactly the members every child hierarchy ends + up binding identically. It has no role in the C++ API, where the + statically-typed entities are used directly. + + @see https://wowdev.wiki/Main_Page */ + struct [[ + =welder::weld, + =welder::weld_as("FileEntity"), + WOWLIB_CS_FAMILY_SURFACE + =welder::doc(R"( + The version-agnostic root of every file-level entity: the WMO, M2, + Skeleton, ADT, WDT and WDL family bases and the BLP all derive it. + Holds the contract every entity shares (validate / ensure_valid), so a + heterogeneous collection of loaded files can be processed uniformly; + reading and writing stay on the per-format types, whose signatures + differ.)") + ]] FileEntityBase + { + }; +} diff --git a/src/wowlib/formats/m2/m2.hpp b/src/wowlib/formats/m2/m2.hpp index 5fd55ba..ae0bf3e 100644 --- a/src/wowlib/formats/m2/m2.hpp +++ b/src/wowlib/formats/m2/m2.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -59,7 +60,7 @@ namespace wowlib::formats::m2 its satellite files (.skin, .anim) baked in. Construct the concrete version with M2.for_version(expansion), then read()/write(); the per-version M2* classes are subclasses. See https://wowdev.wiki/M2.)") - ]] M2Base + ]] M2Base : FileEntityBase { bool operator==(const M2Base&) const = default; }; diff --git a/src/wowlib/formats/m2/skeleton.hpp b/src/wowlib/formats/m2/skeleton.hpp index e1429cc..9dd431b 100644 --- a/src/wowlib/formats/m2/skeleton.hpp +++ b/src/wowlib/formats/m2/skeleton.hpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -215,7 +216,7 @@ namespace wowlib::formats::m2 version. Construct a concrete version with Skeleton.for_version(expansion); the per-version Skeleton* classes are subclasses. See https://wowdev.wiki/M2/.skel.)") - ]] SkeletonBase + ]] SkeletonBase : FileEntityBase { bool operator==(const SkeletonBase&) const = default; }; diff --git a/src/wowlib/formats/wdl/wdl.hpp b/src/wowlib/formats/wdl/wdl.hpp index 8a22737..02ce506 100644 --- a/src/wowlib/formats/wdl/wdl.hpp +++ b/src/wowlib/formats/wdl/wdl.hpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -64,7 +65,7 @@ namespace wowlib::formats::wdl version with WDL.for_version(expansion), then read()/write(); the per-version WDL* classes are subclasses. See https://wowdev.wiki/WDL.)") - ]] WDLBase + ]] WDLBase : FileEntityBase { }; diff --git a/src/wowlib/formats/wdt/wdt.hpp b/src/wowlib/formats/wdt/wdt.hpp index 50efb91..8847c21 100644 --- a/src/wowlib/formats/wdt/wdt.hpp +++ b/src/wowlib/formats/wdt/wdt.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -51,7 +52,7 @@ namespace wowlib::formats::wdt entity. Construct the concrete version with WDT.for_version(expansion), then read()/write(); the per-version WDT* classes are subclasses. See https://wowdev.wiki/WDT.)") - ]] WDTBase + ]] WDTBase : FileEntityBase { }; @@ -153,6 +154,24 @@ namespace wowlib::formats::wdt const FileKey& key [[=welder::doc("the main file identity; must resolve to a path")]]) const; + [[=welder::doc(R"( + Check the logical integrity contracts this object must satisfy to + LOAD in the client — across the main file AND every engaged + satellite — which write() deliberately never enforces. Call it + before writing when you want to know the files will load. An object + read from a client and left unmodified reports no errors.)"), + =welder::returns(R"(every violated contract, each with its member path + ("root..." / "occlusion..." / "lights..." / + "fogs..." / "particulates..."))")]] + ValidationReport validate() const; + + [[nodiscard]] + [[=welder::doc("Validate and raise on the first error instead of " + "returning a report — the assert-style face of " + "validate()."), + =welder::returns("nothing; raises when validate() finds any error")]] + Result ensure_valid() const; + private: // --- internal fs-I/O helpers (definitions at the bottom of this header) -- @@ -273,6 +292,40 @@ namespace wowlib::formats::wdt return {}; } + template + ValidationReport detail::WDT::validate() const + { + ValidationReport report; + { + const std::size_t mark = report.size(); + formats::detail::validate_entity(root, report); + report.prefix_from(mark, "root"); + } + // Each engaged satellite validates under its member path; the version + // ranges the entity does not carry cost nothing to gate on. + const auto validate_part = [&report](const auto& part, std::string_view name) + { + const std::size_t mark = report.size(); + formats::detail::validate_entity(part, report); + report.prefix_from(mark, std::string{name}); + }; + if constexpr (requires { this->occlusion; }) + validate_part(this->occlusion, "occlusion"); + if constexpr (requires { this->lights; }) + validate_part(this->lights, "lights"); + if constexpr (requires { this->fogs; }) + validate_part(this->fogs, "fogs"); + if constexpr (requires { this->particulates; }) + validate_part(this->particulates, "particulates"); + return report; + } + + template + Result detail::WDT::ensure_valid() const + { + return validate().to_result(); + } + template Result detail::WDT::write(fs::FileSystem& fs, const FileKey& key) const { diff --git a/src/wowlib/formats/wmo/wmo.hpp b/src/wowlib/formats/wmo/wmo.hpp index ff2dbee..f192892 100644 --- a/src/wowlib/formats/wmo/wmo.hpp +++ b/src/wowlib/formats/wmo/wmo.hpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -48,7 +49,7 @@ namespace wowlib::formats::wmo file and all its group files as one entity. Construct the concrete version with WMO.for_version(expansion), then read()/write(); the per-version WMO* classes are subclasses. See https://wowdev.wiki/WMO.)") - ]] WMOBase + ]] WMOBase : FileEntityBase { }; diff --git a/tests/csharp/FamilySurfaceTests.cs b/tests/csharp/FamilySurfaceTests.cs index d7eeb61..f2ce2db 100644 --- a/tests/csharp/FamilySurfaceTests.cs +++ b/tests/csharp/FamilySurfaceTests.cs @@ -76,6 +76,31 @@ public void MethodsDispatchOnTheBase() Assert.NotNull(report); } + [Fact] + public void FileEntityRootRunsTheSharedContract() + { + // Every file-level entity derives Formats.FileEntity; the multi-level + // family surface hoists the contract they ALL bind (Validate / + // EnsureValid), so a heterogeneous collection processes uniformly — + // WMO family member and unversioned BLP alike. + using var wmo = Formats.WMO.WMO.ForVersion(Expansion.Wotlk); + using var blp = new Formats.BLP.BLP(); + var entities = new Formats.FileEntity[] { wmo, blp }; + foreach (var e in entities) + { + using var report = e.Validate(); + Assert.NotNull(report); + } + // fs Read/Write do NOT leak to the root: ADT's signature differs, so + // the intersection correctly excludes them. + Assert.Null(typeof(Formats.FileEntity).GetMethod("Read")); + Assert.NotNull(typeof(Formats.WMO.WMO).GetMethod("Read")); + // The copy-constructor idiom replaced Clone(). + Assert.Null(typeof(Formats.BLP.BLP).GetMethod("Clone")); + using var copy = new Formats.BLP.BLP(blp); + Assert.NotNull(copy); + } + [Fact] public void BareBaseInstanceThrowsFromTheDispatchDefaultArm() { From 5ac7497821955a00baa893e1b9b695e8dc6ef8b4 Mon Sep 17 00:00:00 2001 From: Sergey Shumakov Date: Sat, 22 Aug 2026 23:07:40 +0300 Subject: [PATCH 2/4] =?UTF-8?q?csharp:=20blittable=20Data=20mirrors=20?= =?UTF-8?q?=E2=80=94=20the=20renderer-profile=20fix=20for=20per-vertex=20r?= =?UTF-8?q?ecords?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WoWRenderLib profile: ~15% of ParseADT inside McnrEntry.get_Normal / Vector.get_Item / FixedArray.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/FixedArray gain AsSpan() — 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() 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 --- cmake/Dependencies.cmake | 2 +- docs/content/guide/maps.md | 24 ++++++++++++++++++++++++ tests/csharp/FamilySurfaceTests.cs | 18 ++++++++++++++++++ tests/csharp/WoWLib.Tests.csproj | 2 ++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index 3e5b962..208e99a 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -154,7 +154,7 @@ FetchContent_Declare(welder # — welder core resolves those through name_of, no rod feature involved. FetchContent_Declare(welder_csharp GIT_REPOSITORY https://github.com/skarndev/welder-csharp.git - GIT_TAG 772e6c9cf35b7b7a255d1831bf50584749e7f2d5) + GIT_TAG 7f847607bd947eb9f2e2236e6206c9521a496567) # --- stb_dxt (BLP DXT/BC compression; single public-domain header) --- # Pinned to the last commit that touched stb_dxt.h (2021-07-12); the URL_HASH diff --git a/docs/content/guide/maps.md b/docs/content/guide/maps.md index 18d3724..0454889 100644 --- a/docs/content/guide/maps.md +++ b/docs/content/guide/maps.md @@ -183,3 +183,27 @@ including WotLK — Outland tiles still use it) lives on `chunk.legacy_liquid`. See the **[ADT](../python/adt/index.md)**, **[WDT](../python/wdt/index.md)** and **[WDL](../python/wdl/index.md)** format pages for the authoritative shapes and per-field version badges. + +## Bulk access to per-vertex records (C#) + +Per-vertex chunk data — heights, normals, colors — is hot-path material for +renderers, and element-wise access through the live views pays interop cost +per element. Every POD record type carries a blittable `Data` mirror of the +native layout; reinterpret the whole buffer as one span instead: + +```csharp +using WoWLib; + +foreach (var chunk in tile.Chunks) +{ + // ONE interop crossing for all 145 normals, zero allocations: + var normals = chunk.Normals.AsSpan(); + for (int i = 0; i < normals.Length; i++) + Emit(normals[i].Normal[0], normals[i].Normal[1], normals[i].Normal[2]); +} +``` + +The same applies to any scalar vector (`Indices`, `Heights`, …) through the +non-generic `AsSpan()`. Both spans are zero-copy views of C++ memory — valid +until a size-changing operation or `Dispose`, and writable (edits land +directly in the entity). diff --git a/tests/csharp/FamilySurfaceTests.cs b/tests/csharp/FamilySurfaceTests.cs index f2ce2db..58e1645 100644 --- a/tests/csharp/FamilySurfaceTests.cs +++ b/tests/csharp/FamilySurfaceTests.cs @@ -101,6 +101,24 @@ public void FileEntityRootRunsTheSharedContract() Assert.NotNull(copy); } + [Fact] + public unsafe void BlittableMirrorsCoverTheHotRecords() + { + // The renderer-profile fix: per-vertex records read in BULK — one + // interop crossing per buffer via the blittable Data mirror, instead + // of several per element through live views. + using var normals = new Vector(); + for (int i = 0; i < 4; i++) + using (var e = new Formats.ADT.Chunks.McnrEntry()) + normals.Add(e); + var span = normals.AsSpan(); + Assert.Equal(4, span.Length); + span[2].Normal[1] = -7; // straight to native + Assert.Equal(-7, normals[2].Normal[1]); // seen by the live view + Assert.Throws( + () => normals.AsSpan()); // size gate holds + } + [Fact] public void BareBaseInstanceThrowsFromTheDispatchDefaultArm() { diff --git a/tests/csharp/WoWLib.Tests.csproj b/tests/csharp/WoWLib.Tests.csproj index 413a55b..910c523 100644 --- a/tests/csharp/WoWLib.Tests.csproj +++ b/tests/csharp/WoWLib.Tests.csproj @@ -17,6 +17,8 @@ has (a dotnet 10 SDK need not carry the 8.0 runtime). --> Major enable + + true false $(MSBuildThisFileDirectory)../../build/csharp/bindings/WoWLib/WoWLib.csproj From 85a7c8061ba897aadf7bbc23216659c660b77b23 Mon Sep 17 00:00:00 2001 From: Sergey Shumakov Date: Sat, 22 Aug 2026 23:42:54 +0300 Subject: [PATCH 3/4] =?UTF-8?q?csharp:=20AsDataSpan()=20=E2=80=94=20the=20?= =?UTF-8?q?mirror=20span=20without=20the=20type=20argument?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User review of the bulk API: the AsSpan() type argument is a C# limitation (no associated types — Vector 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 type — extensions now claim per element). Guide + FamilySurfaceTests use the sugar; pin bumped; dotnet test 35/35. Co-Authored-By: Claude Fable 5 --- cmake/Dependencies.cmake | 2 +- docs/content/guide/maps.md | 2 +- tests/csharp/FamilySurfaceTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index 208e99a..cdf854f 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -154,7 +154,7 @@ FetchContent_Declare(welder # — welder core resolves those through name_of, no rod feature involved. FetchContent_Declare(welder_csharp GIT_REPOSITORY https://github.com/skarndev/welder-csharp.git - GIT_TAG 7f847607bd947eb9f2e2236e6206c9521a496567) + GIT_TAG ad87e5041f9aa21893a5d767f5ab8dd2489d9a9f) # --- stb_dxt (BLP DXT/BC compression; single public-domain header) --- # Pinned to the last commit that touched stb_dxt.h (2021-07-12); the URL_HASH diff --git a/docs/content/guide/maps.md b/docs/content/guide/maps.md index 0454889..8f835f2 100644 --- a/docs/content/guide/maps.md +++ b/docs/content/guide/maps.md @@ -197,7 +197,7 @@ using WoWLib; foreach (var chunk in tile.Chunks) { // ONE interop crossing for all 145 normals, zero allocations: - var normals = chunk.Normals.AsSpan(); + var normals = chunk.Normals.AsDataSpan(); // == AsSpan() for (int i = 0; i < normals.Length; i++) Emit(normals[i].Normal[0], normals[i].Normal[1], normals[i].Normal[2]); } diff --git a/tests/csharp/FamilySurfaceTests.cs b/tests/csharp/FamilySurfaceTests.cs index 58e1645..b40d265 100644 --- a/tests/csharp/FamilySurfaceTests.cs +++ b/tests/csharp/FamilySurfaceTests.cs @@ -111,7 +111,7 @@ public unsafe void BlittableMirrorsCoverTheHotRecords() for (int i = 0; i < 4; i++) using (var e = new Formats.ADT.Chunks.McnrEntry()) normals.Add(e); - var span = normals.AsSpan(); + var span = normals.AsDataSpan(); // the generated sugar for AsSpan() Assert.Equal(4, span.Length); span[2].Normal[1] = -7; // straight to native Assert.Equal(-7, normals[2].Normal[1]); // seen by the live view From 75063748bb912d9a8c7613fe99358a6e02575a69 Mon Sep 17 00:00:00 2001 From: Sergey Shumakov Date: Sat, 22 Aug 2026 23:56:16 +0300 Subject: [PATCH 4/4] csharp: welder-csharp pin -> 1eea11a, the AsDataSpan merge on main Tree byte-identical to the ad87e50 branch head this previously pinned. Co-Authored-By: Claude Fable 5 --- cmake/Dependencies.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index cdf854f..203866e 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -154,7 +154,7 @@ FetchContent_Declare(welder # — welder core resolves those through name_of, no rod feature involved. FetchContent_Declare(welder_csharp GIT_REPOSITORY https://github.com/skarndev/welder-csharp.git - GIT_TAG ad87e5041f9aa21893a5d767f5ab8dd2489d9a9f) + GIT_TAG 1eea11a4f0a8404db659ba97e2e75f9c01d1586b) # --- stb_dxt (BLP DXT/BC compression; single public-domain header) --- # Pinned to the last commit that touched stb_dxt.h (2021-07-12); the URL_HASH