Historical record: all phases are complete. For current builds, tests, and active limitations, start with NEXT.md and CONTRIBUTING.md. Dated task notes below are not a current work queue. References below to numbered entries of the old
NEXT.mdrefer to the archived long handoff.
Status: APPROVED (2026-07-12 by Robert Vokáč) — ALL 10 PHASES COMPLETE (2026-07-14). The
port is done. Phase 7's former architectural blocker (TilemapRenderer/
TilemapWorldRenderer, VertexPositionColorTexture vs DefaultEffect vertex-layout
mismatch) was resolved using cna's real BasicEffect; Phase 10 (end-to-end example,
Doxygen pass, README/NOTICE.md refresh) closed out the plan. See each phase's entry for
full history.
Fidelity requirement: port 1:1 wherever C#/C++ differences allow — no simplification. See
NEXT.md for current state and remaining follow-up items (none blocking).**
cna-extended is a C++23 port of MonoGame.Extended
(the craftworkgames/MonoGame.Extended repository), adapted to plug into
cna (a C++23 port of XNA 4.0 / FNA) and built on top of
sharp-runtime (a C++23 reimplementation of the relevant .NET BCL
surface). It is a sibling repository to cna, sharp-runtime, and easy-3d, following
the same conventions.
Reference source (full clone, kept up to date, read-only, never edited):
/rv/data/library/github.com/craftworkgames/MonoGame.Extended
(update with /rv/data/library/github.com/github.sh — no-arg mode refreshes all clones).
- Math, Shapes & Collections foundation — root-level math/bounding/interface types,
Math/,Collections/(minusObservableCollection, see below). - Input, Timers, Tweening, ViewportAdapters, VectorDraw — small independent utility modules.
- Collisions 2D —
Collision2D/CollisionShape2Droot types +Collisions/(broadphase:QuadTree,SpatialHash; narrow phase:CollisionWorld2D,ICollisionActor). - Screens — screen/scene stack manager.
- Graphics & BitmapFonts —
SpriteBatchextensions, sprites/sprite sheets, texture atlases (including the direct-JSON TexturePacker reader), nine-patch, custom effects, BMFont bitmap fonts, and theAnimations/frame-animation-controller module (paired withGraphicssince it exists to drive sprite-sheet animation). - Particles — the Mercury-Particle-Engine-derived particle system. (License note: all
files in this module carry MonoGame.Extended's own MIT header — see
NOTICE.md.) - ECS — the Artemis-style Entity-Component-System (
World/Entity/Aspect/Systems). - Tilemaps — Tiled (TMX/JSON), LDtk, and Ogmo map formats and rendering, plus the
Serialization/helpers they depend on.
MonoGame.Extended.Content.Pipeline(the whole design-time MGCB assembly) — the user decided up front this will not be ported; it only matters at asset-build time inside a .NET/MSBuild toolchain and has no C++ runtime counterpart.- Runtime
.xnbcontent readers:Content/ContentReaders/*(BitmapFontContentReader,Texture2DAtlasReader,ParticleEffectContentReader,JsonContentTypeReader), theTilemaps/Content/*Readerclasses, andContent/ExtendedContentManager.cs/Content/ContentReaderExtensions.cs(glue code that only exists to register those xnb readers). Rationale: without the excluded Content Pipeline there is no way to produce the.xnbfiles these classes read, so porting them would be dead code. Direct-format loading (Tiled TMX/JSON, LDtk JSON, Ogmo JSON, TexturePacker JSON, BMFont.fnt) covers the same ground.Content/ExternalResourceResolver(s).cs(path resolution for external tileset/image references) is not xnb-specific and is ported, as part of the Tilemaps phase. Correction (Phase 5):Content/BitmapFonts/{BitmapFontFileContent, BitmapFontFileReader}.cswas originally listed here as "the xnb-side helper" forBitmapFonts/— that was wrong, caught while actually implementing Phase 5. Both files were read directly:BitmapFontFileReaderis a direct parser for the AngleCode BMFont.fntspec (binary/text/XML variants) with no xnb/ContentReader/ContentManager dependency at all. It is ported, as part of Phase 5 — see that phase's task list. Reversed (2026-07-14): the project owner explicitly requested both be ported anyway, as a genuinely separate type alongsideObservableCollection/IObservableCollectionfrom MonoGame.Extended's ownCollections/—sharp-runtimealready has the real BCL type atSystem::Collections::ObjectModel::ObservableCollection. Use that instead.sharp-runtime's (different event shape — four separate events vs. oneCollectionChangedwith a change payload). See the Collections checklist entry below (Phase 1) for what actually landed.
- MonoGame.Extended is MIT-licensed (Copyright 2015–2024 Dylan Wilson, Lucas
Girouard-Stranks, Christopher Whitley; all individual source files carry a
"Copyright (c) Craftwork Games... Licensed under the MIT license" header). MIT permits
porting/relicensing a derivative work under a new MIT copyright as long as the
original notice is preserved somewhere — this is done via
NOTICE.md, mirroring the patterncnauses inTHIRD_PARTY_NOTICES.mdfor its FNA attribution. cna-extendeditself will be MIT licensed, copyright Robert Vokáč, matchingeasy-3d'sLICENSE.NOTICE.md(Phase 0) will carry: the full MonoGame.Extended MIT notice, a link back to the upstream repo, and a courtesy mention of MonoGame.Extended's own "Special Thanks" chain (Mercury Particle Engine, 2D XNA Primitives, LibGDX, Nez) since the Particles module traces its lineage there even though the code itself ships under MonoGame.Extended's own MIT header.- Every ported source file gets an SPDX header (
// SPDX-License-Identifier: MIT) plus a short "ported from MonoGame.Extended" note, matchingsharp-runtime's convention for its "ported from .NET runtime" headers.
- C++ standard: C++23, matching
cna/sharp-runtime/easy-3d. - Namespace:
CNA::Extended::(decided 2026-07-12), sub-namespaced per module, e.g.CNA::Extended::Tilemaps,CNA::Extended::Particles,CNA::Extended::ECS,CNA::Extended::Graphics. Rationale:cna-extendedis a non-official addition on top of the XNA-compatible core, consistent withcna's ownNOXNA/CNA::convention for non-XNA additions — it does not claim to be an official MonoGame namespace. - File layout: header/source split mirroring the namespace path, matching
cnaandsharp-runtime:include/CNA/Extended/<Module>/<Type>.hpp↔src/CNA/Extended/<Module>/<Type>.cpp. Simple types may be header-only. - Properties:
getXProperty()/setXProperty(), matchingsharp-runtime/cna. - Types: use
sharp-runtimetype aliases (SharpRuntime::intcs, etc.) and BCL types (System::Collections::Generic::List,System::EventHandler,std::ranges— notsharp-runtime's legacyLinq.hpp, persharp-runtime's ownCLAUDE.mdrule 8) in public APIs, same ascna. - CMake integration pattern: mirror
easy-3d's sibling-dependency pattern (the cleanest precedent in this ecosystem):add_library(CNA_EXTENDED STATIC ...), aliasCNA::Extended(or similar — finalize exact target/alias names in Phase 0).- If a
CNAtarget already exists (parent build didadd_subdirectory(../cna)first), just link against it. - Else, if
CNA_EXTENDED_LINK_CNAis set, buildcnafrom../cnaviaadd_subdirectory. - Else, fall back to a headers-only include path (
CNA_EXTENDED_CNA_INCLUDE_DIR) for compile-checking without a full link, same aseasy-3d's test/example pattern. - Same three-tier resolution for
sharp-runtime.
- Tests: GoogleTest, mirroring
sharp-runtime's andcna's test setup (submodule atvendor/googletest). Port the upstream test suite (tests/MonoGame.Extended.Tests/**) alongside each phase's implementation, translated to GoogleTest — do not skip tests to "come back later." - Docs: Doxygen comment blocks on public types/methods, matching
cna/sharp-runtime. - Reference variant: MonoGame.Extended ships three parallel build variants
(
MonoGame.Extended,FNA.Extended,KNI.Extended) gated by#if FNA/#if KNIconditionals. Sincecnamirrors FNA's API surface, prefer theFNAbranch of any conditional as the primary reference; fall back to the default (MonoGame) branch elsewhere. Ignore KNI-only branches unless a specific need arises. - No
plan.sqlite3for now — the user explicitly asked for tasks to live in this markdown file. If the task list grows unwieldy as porting progresses (sharp-runtimeneeded a SQLite DB for its ~450-file scope), revisit that decision with the user rather than silently switching.
Ordered by dependency — later phases build on earlier ones. Check off tasks as they
land; update NEXT.md alongside every session's progress.
-
LICENSE(MIT, Robert Vokáč, matchingeasy-3d'sLICENSE) -
NOTICE.md(MonoGame.Extended MIT attribution, see §3) -
README.md(purpose, relationship tocna/sharp-runtime, scope summary, link to this plan) - Root
CMakeLists.txt(see §4 CMake pattern) — options, target, alias, sibling dependency resolution forcnaandsharp-runtime -
include/CNA/Extended/andsrc/CNA/Extended/skeleton directories -
vendor/googletestsubmodule +tests/CMakeLists.txt -
examples/minimal/smoke-test example (mirroringeasy-3d'sexamples/minimal) -
.gitignore,Doxyfile - Verify the whole scaffold builds green (empty lib, empty test binary) before Phase 1
starts — see
NEXT.mdfor the exact command/output.
No dependency on CNA graphics — pure math/data types. Blocks almost every later phase.
- Marker interfaces:
IMovable,IRotatable,IScalable,ISizable,IRectangular,IColorable,IEquatableByRef(2026-07-12) -
TransformFlags+BaseTransform<TMatrix>+Transform3(2026-07-12) — see §6 for theMulticastActionextension this required. -
Transform2(BaseTransform<Matrix3x2>) — deferred untilMatrix3x2is ported (a few items below in this same phase); itsRecalculateLocalMatrix/RecalculateWorldMatrixbodies needMatrix3x2::CreateScale/CreateRotationZ/CreateTranslation/Multiply/Decompose. Do this as the immediate follow-up to theMatrix3x2task, not standalone. - Bounding volumes:
BoundingBox2D,BoundingCircle2D,BoundingCapsule2D,BoundingPolygon2D,OrientedBoundingBox2D(2026-07-12) — data/geometry API fully ported (fields, properties, factory methods,Transform/Translate/Deconstruct,Equals/GetHashCode/ToString/operators). Two deferrals, both documented in the relevant header's top comment, not silently dropped: - All 16Contains(...)/Intersects(...)/TryGetCollision(...)overloads across the 5 types — every one delegates toCollision2D(Phase 2), not this phase. -BoundingCapsule2D::CreateFromSegment(LineSegment2D, float)andCreateMergedneedLineSegment2D::DistanceToPoint— add both onceLineSegment2Dlands (next task in this phase). Known gap vs. this phase's "port tests faithfully" bar: upstream has ~130 test methods across these 5 types' test files, but ~20-27% per file exercise the deferredCollision2D-dependent methods. Rather than port the ~100 portable upstream tests now and revisit them again onceCollision2Dlands, wrote fresh (not upstream-1:1) tests covering everything that's ported (92 total project tests passing). Follow-up task, do at the start of Phase 2: port the full upstreamtests/MonoGame.Extended.Tests/Primitives/Shapessuites for these 5 types 1:1, in one pass, onceCollision2Dmakes the currently-deferred methods portable too. -
Line2D,LineSegment2D,Ray2D(2026-07-12) — construction/factory methods, geometry (DistanceToPoint/ClosestPoint/GetPoint/GetBounds/Normalize/etc.),Equals/GetHashCode/ToString/operators/Deconstructfully ported for all 3 types. AllIntersects(...)overloads across all 3 types, plusLineSegment2D::DistanceSquaredToPoint/DistanceToPoint/DistanceSquaredToSegment/DistanceToSegment, deferred untilCollision2D(Phase 2) — documented in each header. Correction to theBoundingCapsule2D.hppdeferral note (written beforeLineSegment2Dexisted): portingLineSegment2Ddoes not unblockBoundingCapsule2D::CreateFromSegment/CreateMergedafter all —LineSegment2D::DistanceSquaredToPoint's own body needsCollision2D, so those twoBoundingCapsule2Dmembers are still blocked on Phase 2, same as itsContains/Intersects/TryGetCollisionoverloads.BoundingCapsule2D.hpp's comment has been corrected accordingly. -
Camera<T>(2026-07-12) — fully abstract (all members pure virtual), header-only, ported in full.getBoundingRectangleProperty()returnsRectangleF, forward-declared (not yet ported — a pure virtual declaration doesn't need a complete type). No upstream tests exist for this abstract type; a compile-only smoke test was added, with real instantiation-based tests deferred untilRectangleFlands (a concrete override returningRectangleFby value needs the complete type). -
OrthographicCamera— COMPLETE (2026-07-13), landed very late: unlike theTransform2/Matrix3x2case, this had a deep structural dependency, not a narrow one (OrthographicCamerastores aViewportAdapteras a required field). The plan here always said to port it "immediately afterViewportAdapterslands in Phase 3" —ViewportAdapterslanded in Phase 3 as scheduled, but this specific follow-up was never actually done and nobody noticed until a Phase 7Rendering/*fork needed it as a prerequisite and discovered the gap by reading this very entry. FullCamera<Vector2>+IMovable+IRotatableimplementation, world-bounds/zoom clamping, parallax view matrices, frustum/containment; 51 tests (2GTEST_SKIP()-guarded — see Phase 7'sRendering/*entry for the realcnaBoundingFrustum::Containsbug those two hit). Lesson for future sessions: an explicit "port X immediately after Y" note in this file is not self-enforcing — it needs an actual task/reminder, or it can sit forgotten for many phases (in this case, from Phase 3 all the way to Phase 7) even though every individual phase along the way was completed and verified. - Color helpers:
ColorExtensions,ColorHelper,HslColor(2026-07-13) — ported directly (no fork needed, ~620 lines total).ColorHelper's name→Colorlookup table is built via reflection upstream (typeof(Color).GetRuntimeProperties()); C++ has no runtime reflection, so it's a hand-written (script-generated from CNA'sColor.hpp, cross-checked at 141 entries) static table instead, with lower-cased keys for case-insensitive lookup (matches upstream'sStringComparer.OrdinalIgnoreCase).HslColor::ToRgb'sMathExtended.MachineEpsilonreference resolved directly viastd::numeric_limits<float>::epsilon()(the exact same value,1.19209290e-7f) rather than deferring on the not-yet-portedMathExtended— a trivial constant, not an algorithm, matching the established "duplicate the trivial" pattern. All upstream tests ported 1:1 this time (noCollision2Ddependency to work around):ColorExtensionsTests.cs/ColorHelperTests.cs/HslColorTests.csin full, via GoogleTestTEST_P/INSTANTIATE_TEST_SUITE_Pfor the[Theory]/[InlineData]cases. One real bug caught by the build:ColorHelper::FromAbgrcouldn't use CNA'sColor(UInt32)constructor (private in CNA, unlike upstream's public one) — reworked to decompose into R/G/B/A components and use the public 4-int constructor instead, same resulting color. -
MathExtended,FloatHelper,Angle(2026-07-13) — ported directly (~315 lines).Angle.cscarries its own upstream attribution to the SlimMath project (Copyright (c) 2007-2010 SlimDX Group, MIT) in its file header — not just Craftwork Games; added a proper "code directly derived from other MIT-licensed projects" section toNOTICE.md(with SlimMath's full license text) rather than folding it into the existing inspiration-only "courtesy attribution" list. Fidelity note:Angle::Equals/CompareToare intentionally non-const, takingotherby value — upstream's versions aren'treadonlyand callWrapPositive()on boththisand a by-value copy ofother, so callingEquals/CompareTo/==/!=has the side effect of wrapping the angle into[0, tau).GetHashCode()deliberately does not wrap first, unlikeEquals— an apparent equality/hashing contract inconsistency in upstream itself, preserved as-is rather than fixed. All upstream tests ported 1:1 (MathExtendedTests.cs,AngleTest.cs). Also updatedHslColor.cpp(from the previous task) to use the now-realMathExtended::MachineEpsiloninstead of thestd::numeric_limits<float>::epsilon()placeholder — same value, now the actual named constant matching upstream exactly. -
RectangleF,Rectangle.Extensions,RectangleF.Extensions,BoundingRectangle(2026-07-13, via forked sub-agent) — ported (1313 lines: 4 header/source pairs + 4 test files). Deferred, all documented in the relevant header, all genuinely blocked on not-yet-ported types (SizeF,Matrix3x2,PrimitivesHelper), not skipped for convenience: -RectangleF:Sizeproperty +RectangleF(Vector2, SizeF)ctor (SizeF); bothTransform(...)overloads +CreateFrom(points,...)×2/UpdateFromPoints+SquaredDistanceTo/DistanceTo/ClosestPointTo(Matrix3x2/PrimitivesHelper). -BoundingRectangle: the(Vector2, SizeF)ctor and both implicitBoundingRectangle(Rectangle|RectangleF)conversions (all three route through the deferredSizeFctor upstream);Transform/CreateFrom(points,...)/UpdateFromPoints/SquaredDistanceTo/ClosestPointTo(same reasons asRectangleF). Its inverse conversions (BoundingRectangle→Rectangle/RectangleF) needed noSizeFand are ported. Kept as a C++ aggregate (no user-declared constructor) rather than adding one upstream doesn't have, since C++20 parenthesized-aggregate-init covers both the default and 2-arg cases. -OrientedRectangle— entirely deferred, no file created at the time this task landed. ItsOrientationfield isMatrix3x2itself (not just used in a method) — a deep structural dependency likeOrthographicCamera→ViewportAdapter, not a narrow one. Update (2026-07-13, follow-up task afterMatrix3x2landed): now ported, minus what's still transitively blocked — see the dedicated entry below. Follow-ups landed in the same pass (verified unblocked, not assumed — see §6):IRectangularFinIRectangular.hpp(test added toInterfaceTests.cpp);Camera<T>inCamera.hpp(forward-declare replaced with a real#include,CameraTests.cppgot a full concreteCamera<Vector2>test double replacing the compile-only placeholder).ISizableis still blocked (needsSizeF, unrelated to this task). Test coverage: upstream test suite for this area is unusually thin/broken — the mainPrimitives/RectangleFTests.csis entirely blocked (every test needs the deferredSizeFctor orTransform), andPrimitives/BoundingRectangleTests.csis entirely commented out in upstream itself (nothing active to port). Ported what upstream actually has active and portable 1:1 (7 tests:Math/RectangleFTests.cs+RectangleExtensionsTests.cs), wrote ~30 fresh tests for everything else that's ported but untested/disabled upstream. -
CircleF,EllipseF,Segment2(2026-07-13, via forked sub-agent) — ~860 lines across 9 files.EllipseFfully self-contained, zero deferrals.CircleF: 4Intersects(CircleF, BoundingRectangle)overloads deferred (callBoundingRectangle::SquaredDistanceTo, itself already blocked onPrimitivesHelper— inherited blocker, not new).Segment2: 2Intersects(RectangleF| BoundingRectangle, out Vector2)overloads deferred (PrimitivesHelper. IntersectsSlab, not ported). No whole-type deferral needed this time (unlikeOrientedRectangle). Genuine upstream bug found and faithfully preserved:Segment2.SquaredDistanceTo(Segment2.cs:96) has a bareendToPoint.Dot(endToPoint);expression statement missing itsreturn— for a point that projects beyond the segment'sEnd, this silently falls through to the perpendicular-distance formula instead of returning the correct end-point distance, producing a wrong result (confirmed: e.g. 400 instead of the correct 800 for one straightforward case). The C++ port reproduces this exactly, per the standing no-simplification mandate; documented prominently inSegment2.hpp's header comment and covered by a regression test asserting the actual (bug-preserving) value with a comment showing what the "correct" value would be. Do not silently fix this if it's ever noticed again — it's a deliberate fidelity choice, not an oversight.CircleF.cs/Segment2.cscite "Real-Time Collision Detection, Christer Ericson, 2005" as an algorithm source in comments — an academic citation, not a third-party code/license dependency (unlikeAngle.cs's SlimMath case); noNOTICE.mdchange needed. Test coverage:EllipseFTest.csfully active, ported 1:1 (13 cases);CircleFTests.cshas exactly 1 active upstream test (rest commented out) — ported that one plus ~15 fresh;Segment2DTests.csis entirely commented out upstream (0 portable) — wrote 11 fresh tests, including the bug-regression test above. -
Size,SizeF,Interval,Thickness(2026-07-13, via forked sub-agent) — ~1450 lines across 16 files, fully ported with no deferrals — nothing in these 4 files depends on unported types.Interval<T>is header-only (a template); upstream'swhere T : IComparable<T>constraint has no C++ analogue for primitives, translated to ordinary</==comparisons (the natural C++ equivalent).Thicknessuses getX/setXProperty() accessors (upstream{get;set;}) whileSize/SizeFuse raw public fields (matching upstream's own field-vs-property split per type). All three SizeF-blocked follow-ups verified unblocked and landed in the same pass:ISizable.hpp(forward-declare → real include + round-trip test),RectangleF(addedSizeproperty +RectangleF(Vector2, SizeF)ctor),BoundingRectangle(added(Vector2, SizeF)ctor + both implicit conversions fromRectangle/RectangleF).RectangleF's andBoundingRectangle's other deferrals (Transform,CreateFrom(points),SquaredDistanceTo) remain blocked onMatrix3x2/PrimitivesHelper— unrelated to this task, not touched. No upstream bugs found this time (unlike the previous task'sSegment2case). Test coverage:Math/IntervalTests.csfully active, ported 1:1 (34 tests, noCollision2Ddependency this time);Primitives/Size2Tests.csentirely commented out upstream (0 portable, wrote fresh); no upstreamThicknessTests.csexists at all (wrote fresh). -
Matrix3x2,MatrixExtensions,Vector2Extensions(2026-07-13, via forked sub-agent) — ~1300 lines across 9 files.Matrix3x2fully self-contained, no deferrals. Bonus follow-up landed in the same pass:Transform2fully implemented inTransform.hpp/.cpp(deferred since task 2 of this phase, pending exactly this dependency) — both constructors,IMovable/IRotatable/IScalableimplementation (Transform2 implements these upstream, Transform3 does not — asymmetry preserved faithfully),RecalculateLocalMatrix/RecalculateWorldMatrix,ToString; 8 new tests inTransformTests.cpp. Other follow-up spots checked precisely (not guessed): -RectangleF/BoundingRectangle'sTransform(...)overloads: confirmed still blocked onPrimitivesHelper.TransformRectangle, Matrix3x2 alone wasn't enough. -CircleF/Segment2's existing deferrals: confirmedPrimitivesHelper-only, unaffected by Matrix3x2 landing. -OrientedRectangle: at the time this task landed, still not ported, but its blocker had changed — its constructor andOrientationfield only neededSizeF+Matrix3x2, both now available; only itsTransformmethod needed the still-missingPrimitivesHelper.TransformOrientedRectangle. Update (2026-07-13, picked up immediately as its own small follow-up task): now ported — see the dedicated checklist entry below for exactly what landed vs. what's still deferred. Bug found — this session's own, not upstream's:Matrix3x2::Identitywas initially built fromVector2::UnitX/UnitY/Zero(CNA statics in a different translation unit) — a static-initialization-order fiasco that silently zeroedIdentityat runtime, caught by 2 failing tests. Fixed with literal float values instead. Checked the other 4 similarstatic const X::Empty-style members already in the tree (RectangleF, BoundingRectangle, SizeF, Size) — all safe, none has this cross-TU pattern. Watch for this specific pattern in any futurestatic const Type X = Type(other statics...)declaration. Test coverage:Math/Matrix3x2.cs's 2 tests +Vector2ExtensionsTests.cs's 10 tests ported 1:1;MatrixExtensionshas no upstream test file; wrote ~19 fresh tests for the rest ofMatrix3x2's large, mostly-untested-upstream surface. -
OrientedRectangle(2026-07-13, follow-up task picked up immediately afterMatrix3x2landed, ported directly without a fork) — fields, constructor,Points,Position(get; set throwsstd::logic_error, matching upstream'sNotImplementedException),Equals/GetHashCode/ToString/operators, theOrientedRectangle(RectangleF)converting constructor, and the self-contained SATIntersects(OrientedRectangle, OrientedRectangle)(returns a namedOrientedRectangleIntersection{bool Intersects, Vector2 MinimumTranslationVector}struct — C++'s closest faithful equivalent to upstream's named-tuple return type). Correction found while scoping this task: the previous task's fork report said onlyTransformwas blocked, butBoundingRectangle(the property) andexplicit operator RectangleF(OrientedRectangle)are also transitively blocked — both callRectangleF::Transforminternally, which is itself still blocked onPrimitivesHelper. All three (the staticTransformmethod, theBoundingRectangleproperty, and theRectangleFconversion operator) are deferred together, documented in the header. Test coverage: upstream'sInitializes_oriented_rectangle/Equals_comparisontests ported 1:1; the entire nestedTransformtest class (11 tests) is inapplicable (all test the deferred method); wrote fresh tests forPosition/theRectangleFconversion/Intersects(no upstream coverage for that SAT algorithm specifically). -
FastRandom,RandomExtensions(2026-07-13, ported directly, no fork) — ~410 lines across 6 files, fully ported, no deferrals.FastRandom's private nestedIFastRandomImpl/LinearCongruentialGeneratorImpl/ThreadSafeFastRandomImpl(a bridge/strategy pattern) ported 1:1 viastd::unique_ptr-owned nested types. C#'s[ThreadStatic]→thread_local.Shared's lazy-initialized static property → a function-local static (deliberately, to avoid a repeat of this session'sMatrix3x2::Identitycross-TU static-init-order bug).RandomExtensions's free functions port each algorithm exactly as upstream wrote it (e.g.NextSingleas a literal(float)NextDouble()cast) rather than delegating tosharp-runtime's own, more modernSystem::Random::NextSingle()— that method already exists and is used elsewhere in the codebase, but implements a different algorithm (matching real .NET 6+Random.NextSingle()); using it here would silently change behavior from what this specific upstream file actually does. No upstream tests exist for either file — wrote fresh tests covering deterministic seeding, range bounds for every overload, and theSharedsingleton property. -
PrimitivesHelper(2026-07-13, ported directly, no fork) — fully ported, no deferrals (~145 lines). Fully self-contained (only needsVector2,Matrix3x2,MathExtended, all already ported) — this was the single most-referenced blocker across Phase 1's deferrals; see the follow-up sweep below.ShapeExtensionsdeferred in full, scope reassessed: despite living in theMath/folder upstream,ShapeExtensions.csis entirelySpriteBatchdebug-drawing code (DrawPolygon/DrawLine/DrawCircle/etc., using a liveGraphicsDeviceand a 1x1 white-pixelTexture2D) — a Graphics-phase concern (Phase 5, "Graphics & BitmapFonts"), not a Phase 1 math-utility concern, regardless of its upstream folder location. Needs a real graphics context to test meaningfully (matches this project'sCNA_EXTENDED_CNA_LINKEDtest-gating pattern for anything touchingGraphicsDevice/SpriteBatch). Port it as part of Phase 5, not standalone here. Newly discovered while scoping this task:ShapeExtensions.csdepends onMonoGame.Extended.Shapes.Polygon/Polyline(source/MonoGame.Extended/Shapes/), an entire upstream folder that was never tracked anywhere in this plan — a real gap, not an intentional Phase-1 exclusion. Checked: both types turned out small (178+41 lines) and fully self-contained (onlyVector2/RectangleF, both already ported), so ported them here rather than leaving the gap open —Polygon,Polyline(include/src/tests/CNA/Extended/Shapes/). First use of a C++ sub-namespace in this phase:CNA::Extended::Shapes, mirroring upstream's ownMonoGame.Extended.ShapesC# sub-namespace (unlike theMath/folder types ported so far, which all stayed in the C# root namespace despite their subfolder). 4 of upstream'sPolygonTests.cstests ported 1:1; no upstream tests exist forPolylineorPrimitivesHelper— wrote fresh tests for both. Follow-up sweep afterPrimitivesHelperlanded (verified by reading each header's deferral comment and the actual upstream.cssource, not assumed — see §6 for the full account):Line2D's deferredIntersects(Ray2D/LineSegment2D/...)overloads andLineSegment2D'sDistanceSquaredToPoint/DistanceSquaredToSegmentremain blocked onCollision2Dspecifically (a different, larger Phase-2 type, notPrimitivesHelper) — not unblocked by this task despite both citing similar "Real-Time Collision Detection" algorithm sources; confirmed still blocked, left deferred. The following were genuinely unblocked and landed in this same task/commit: -RectangleF:Transform(2 overloads),CreateFrom(points, ...)(2 overloads),UpdateFromPoints,SquaredDistanceTo,DistanceTo,ClosestPointTo. Tests added: the 2 constructor tests + 5Transformtests from upstream'stests/MonoGame.Extended.Tests/Primitives/RectangleFTests.cs(ported 1:1; that file'sRectangle_Intersects_Testwas intentionally not ported — it exercises base XNA/FNARectangle.Intersects, not anything MonoGame.Extended adds), plus fresh tests forCreateFrom(points)/UpdateFromPoints/SquaredDistanceTo/DistanceTo/ClosestPointTo(no upstream coverage for those specifically). -BoundingRectangle:CreateFrom(points, ...)(2 overloads),Transform(2 overloads, matching upstream's ref-mutates-input-in-place semantics faithfully),UpdateFromPoints,SquaredDistanceTo,ClosestPointTo. Fresh tests added (the upstreamPrimitives/BoundingRectangleTests.csis entirely commented out, as noted by the pre-existing test file's header comment). -OrientedRectangle:getBoundingRectangleProperty()(upstream:(RectangleF)this),static Transform(OrientedRectangle, ref Matrix3x2), andexplicit operator RectangleF(OrientedRectangle). Upstream'sprivate staticref-takingTransformoverload isn't part of its public API contract, so its logic was inlined directly into the public staticTransform. Ported upstream's full nestedTransformtest class (9 tests) 1:1. Testing nuance found and preserved faithfully: 2 of those upstream tests (Points_are_translated,Applies_rotation_and_translation) use upstream's ownCollectionAssert.Equaltest helper (tests/MonoGame.Extended.Tests/CollectionAssert.cs), which is an order-insensitive containment check (same count + every actual element present in expected), not an ordered sequence comparison — unlike gtest'sEXPECT_EQon astd::vector. A literalEXPECT_EQport of those two tests' expected-point lists failed even though the underlyingOrientedRectangle::Transform/getPointsProperty()math was correct (upstream's own hand-written expected order doesn't match its ownPointsgetter's algorithmic order either — it only passes upstream becauseCollectionAssert.Equaldoesn't check order). Added a small localExpectUnorderedPointsEqualhelper replicating that exact upstream semantics for those 2 tests, rather than reordering the expected values to hide the discrepancy — this is a faithful port of what upstream's test actually asserts, not merely of its C# syntax. - Widened the sweep past the 3 explicitly-flagged types by grepping the tree for "PrimitivesHelper" in deferral comments: found and landedCircleF'sIntersects(CircleF, BoundingRectangle)(collapsed from upstream's 4 ref/value overloads to 1 static + 1 instance, matching the existing CircleF-vs-CircleF convention) andSegment2'sIntersects(RectangleF|BoundingRectangle, out Vector2)(2 overloads, usingPrimitivesHelper.IntersectsSlabdirectly). Fresh tests for both (upstream's own coverage is entirely commented out for both). -
Math/Triangulation/*(2026-07-13, ported via a forked sub-agent) — all 6 upstream files (Vertex,LineSegment,Triangle,CyclicalList,IndexableCyclicalLinkedList,Triangulator+ theWindingOrderenum) fully ported into a newCNA::Extended::Triangulationsub-namespace (matching theShapessub-namespace precedent from task 22), no deferrals. Different license provenance than every other file so far: all 6 upstream files citeMIT Licensed: https://github.com/nickgravelyn/Triangulator(an ear-clipping triangulation implementation), the same "code directly derived from a different MIT project" situation asAngle.cs/SlimMath — handled directly (not delegated to the fork) since it required license research: the original repo now 404s on GitHub (web UI and API both checked), soNOTICE.md's new entry uses a standard MIT template crediting nickgravelyn with an explicit provenance caveat, rather than a verbatim-unavailable original copyright line. All 6 ported files carry a matching SPDX header crediting nickgravelyn, seeNOTICE.md.CyclicalList<T>/IndexableCyclicalLinkedList<T>design: upstream implements these asList<T>/LinkedList<T>subclasses using C#'snew-keyword method hiding for their cyclical indexers — both are purely internal toTriangulator(upstream's own doc comment: "the sole public class in the entire library"), so rather than replicate that hiding mechanism, they compose (not inherit)sharp-runtime'sSystem::Collections::Generic::List<T>/LinkedList<T>, per this project's "reuse sharp-runtime, don't re-roll" convention, adding only the cyclicaloperator[]/RemoveAt/IndexOfon top. Header-only templates, matchingInterval<T>'s precedent. Critical fidelity point: upstream'sTriangulatorhas 5static readonlymutable buffer fields shared across every call — a deliberate (if leaky) upstream design tradeoff for reduced per-call GC pressure, explicitly explained in its own doc comment, makingTriangulate/CutHoleInShapenon-reentrant and not thread-safe by design. Ported as genuinestatic inlineC++ class members (not locals, notthread_local) to preserve that exact tradeoff, documented prominently inTriangulator.hpp's header comment.IsReflexis dead code upstream (never called inTriangulator.cs) — ported anyway for completeness as a private static method (no compiler warning, unlike an unused free function). Independently re-verified (not just trusting the fork's self-report): grepped all 6 upstream.csfiles forpublic |internalmembers and confirmed every one has a mapped fate; hand-traced the trickiest method (CutHoleInShape's nullable-comparison/index-injection logic) line-by-line against the C# source; did a cleanrm -rf build && cmake … && cmake --build …from scratch to confirm zero new compiler warnings (not just trusting a possibly-cached prior build). Test suite grew from 464 to 503 (39 net new tests): upstream's one active test file (TriangulatorTests.cs, 4DetermineWindingOrdertests, including a shoelace-formula regression test for issue #791) ported 1:1; fresh tests for everything else (no upstream coverage exists forTriangulate,CutHoleInShape,EnsureWindingOrder,ReverseWindingOrder, or any ofVertex/LineSegment/Triangle/the two cyclical collection types). Both build configurations verified clean, zero warnings, independently confirmed (503/503ctest). -
GameTimeExtensions,GameComponentCollectionExtensions(2026-07-13, ported directly, no fork) — both fully ported, no deferrals; small (two files, ~36 lines of C# total).GetElapsedSeconds-> a free function takingconst GameTime&.GameComponentCollectionExtensions's twoAdd<T>overloads -> free function templates takingGameComponentCollection&, returningT*(matchesGameComponentCollection's own non-owningIGameComponent*-based design in CNA; confirmed by readingGameComponentCollection.cppthat it neverdeletes its items — caller owns component lifetime, same as C#'s GC ownership model translated to explicit pointers). C#'sFunc<T> createGameComponent->std::function<T*()>, matching this project's establishedFunc<T>convention (seeHslColor.hpp'sMatch/Map). No upstream tests for either file — wrote fresh tests. Both build modes clean,ctest→ 507/507 (was 503). -
FramesPerSecondCounter+FramesPerSecondCounterComponent(2026-07-13, ported directly, no fork) — both fully ported, no deferrals.FramesPerSecondCounterimplements CNA'sIUpdateable(System::Objectbase +EnabledChanged/UpdateOrderChangedas publicSystem::EventHandlermembers +getEnabledChangedEvent()/getUpdateOrderChangedEvent()), following the exact pattern already established by CNA's ownGameComponent.hpp/.cpp(firstSystem::Object-deriving type incna-extended). Likely upstream bug, preserved for fidelity: theUpdateOrdersetter raisesEnabledChanged, notUpdateOrderChanged-- almost certainly a copy-paste error from theEnabledsetter immediately above it inFramesPerSecondCounter.cs. Reproduced exactly, documented prominently in the header, covered by a regression test that names the discrepancy explicitly (matches theSegment2.SquaredDistanceToprecedent from task 18)._oneSecondTimeSpan(static readonlyupstream) ported as a local value constructed where needed rather than astatic constclass member, avoiding any static-initialization-order risk (theMatrix3x2::Identitylesson from task 20) -- a storage-duration simplification only, not a logic change, since the value never varies.FramesPerSecondCounterComponent(DrawableGameComponentsubclass, forwardsUpdate/Drawto an internalFramesPerSecondCounter) requires a liveGame&to construct; ported without a dedicated test file, matching CNA's own established precedent for this exact situation (cna's ownDrawableGameComponentTests.cpp: "No tests: DrawableGameComponent requires a live Game and GraphicsDevice (SDL/GPU)."). All the independently-testable logic lives inFramesPerSecondCounter, which is fully unit tested. No upstream tests exist for either file. Both build modes clean,ctest→ 516/516 (was 507). -
SimpleGameComponent+SimpleDrawableGameComponent(2026-07-13, ported directly, no fork) — both fully ported, no deferrals. Lighter-weight abstract bases (don't need aGame&, unlike CNA's ownGameComponent/DrawableGameComponent), each implementing multiple CNA interfaces directly (IGameComponent,IUpdateable,System::IDisposable,System::IComparable<GameComponent>,System::IComparable<SimpleGameComponent>for the first;+IDrawablefor the second). Explicit-interface-implementation translation: upstream uses C#'s explicit interface implementation twice (bool IUpdateable.Enabled => _isEnabled;alongside publicIsEnabled;bool IDrawable.Visible => _isVisible;alongside publicVisible) — C++ has no direct equivalent, approximated per-case:IsEnabled/Enabledgenuinely differ in name upstream, so ported as two distinct C++ members (a publicgetIsEnabledProperty()plus a private override ofIUpdateable::getEnabledProperty(), callable only through anIUpdateable&reference via virtual dispatch — the closest C++ analog to "interface-only access");Visible/IDrawable.Visibleshare the identical name upstream (nothing distinct to preserve), so collapsed to one publicgetVisibleProperty()override satisfyingIDrawabledirectly — both getters return the same field in both cases regardless, so neither translation changes observable behavior. No upstream tests exist for either file — wrote fresh tests (via minimal concrete test subclasses, since both are abstract) covering enabled/visible/update-order/draw- order change events (raised only when the value actually changes),Initialize()/Dispose()idempotency, andCompareToordering. Both build modes clean,ctest→ 529/529 (was 516). - Collections:
Bag<T>,Deque<T>(2026-07-13, ported via a forked sub-agent) — both fully ported, no deferrals. First use of a newCNA::Extended::Collectionssub-namespace. Both are header-only templates (matchingInterval<T>/Triangulation's established convention for generic C# types).Bag<T>has yet another different license than everything ported so far: unlike every other file (MonoGame.Extended MIT, or theAngle/TriangulationMIT-from-a-different-project cases),Bag.cs's own header credits a 2-clause-BSD-style license — GAMADU.COM's C# port (2013) of thelinuxlich's artemis_CSharpBag<T>. Handled the license research myself (not delegated): unlike thenickgravelyn/Triangulatorcase,artemis_CSharpis still live — fetched its actualBag.csdirectly from the source repository and confirmed the license text matches MonoGame.Extended's embedded copy verbatim, word for word. Added a NEWNOTICE.mdsection ("Code directly derived from other permissively-licensed (non-MIT) projects"), distinct from the existing MIT-project section, since BSD-2-clause is a genuinely different license family from MIT, not just a different copyright holder.Deque<T>implementssharp-runtime's ownSystem::Collections::Generic:: IList<T>, matching howsharp-runtime's ownList<T>implements the same interface. Two mechanical, no-behavioral-consequence simplifications documented inBag.hpp's header (upstream's_isPrimitiveGC-hygiene-only array-clearing skip; upstream'sBagEnumerator/boxing-avoidance enumerator triad, replaced by plainbegin()/end(), since C++ range-basedfornever has the C#-specific boxing-allocation problem that machinery exists to solve). Confirmed and preserved upstream bugs, the most significant found in this port so far (all independently re-verified by hand-tracing concrete examples against the actual upstream algorithm before trusting the fork's claims — see §6 for the full account and specific traced examples): 1.IndexOf's final index-computation formula doesn't check for "not found" (-1) before applying modulo arithmetic, so a genuinely-absent item can makeIndexOfreturn a bogus non-negative index instead of-1— which then makesRemoveeither throw unexpectedly or silently remove an unrelated, real element while reporting success, depending on the buffer's current wraparound offset. A freshly-constructed, never-grownDequeadditionally hits a literal divide-by-zero in this same formula (C#: catchableDivideByZeroException; C++: UB, so explicitly guarded and thrown asstd::domain_errorinstead, since UB itself cannot be "faithfully preserved" as an observable behavior). 2. & 3.RemoveAt's middle-index removal (choosing "shift the shorter half") is only reliable in one of its three practical cases: front-half-shift on a non-wrapped buffer works correctly; back-half-shift is broken even on a non-wrapped buffer (its shift source,arrayCenterIndex, is a fixed physical midpoint with no relationship to the actual removal point); front-half-shift is ALSO broken once the buffer has wrapped (an element is silently lost/ orphaned, replaced by a stale default value). Not caught by upstream's own test suite because its one relevant test only assertsCountdecrements correctly, never that the resulting values are correct. All three preserved exactly (not fixed), each with a dedicated, hand-verified regression test. Smaller preserved quirks:CopyTovalidates the destination-index bound unconditionally, even before theCount == 0no-op check (so copying into an empty-but-valid destination can throw rather than succeed as a no-op);IList<T>::Insertalways throws (Deque<T>only supports front/back insertion), matching theOrientedRectangle::setPositionProperty"matches upstream, always throws" precedent from task 17. Iteration required real care: upstream'sGetEnumerator()re-reads_frontArrayIndex/Countlive on every loop-condition check (not just once at iteration start) — deliberate, and covered by an upstream test (Deque_ForEach_Iteration_Modified) that removes from the front during iteration and expects every remaining element to still be visited exactly once. A naive C++ translation that snapshots state once atbegin()would not reproduce this. Solved by capturing the physical starting offset/capacity once, tracking a visited-count, and re-deriving both the current element and the loop-termination condition from live state on every step — verified to collapse upstream's two differently-shaped loops (wrapped vs. non-wrapped) into one identical physical-index formula. Ported all 17 active upstreamDequeTests.cstests 1:1, plus 24 fresh tests including explicit regressions for all three bugs above and the live-iteration behavior.Bag<T>'s one upstream test is a C#-GC-allocation-boxing benchmark with no C++ equivalent concept — not ported; 19 fresh tests added instead covering actual correctness. Test suite grew from 529 to 589 (60 net new tests). Both build configurations verified clean from a fully clean rebuild (not a cached one), zero new compiler warnings, independently re-confirmed (not just trusting the fork's self-report). - Collections:
ObjectPool<T>,Pool<T>,IPoolable,ItemEventArgs(2026-07-13, ported directly, no fork; small, 244 lines of C# total). All four fully ported, no deferrals; header-only templates.IPoolable::NextNode/PreviousNode->IPoolable*(ObjectPool<T>static_casts toT*at every point upstream does an unchecked(T)node.NextNodecast, trusting the same invariant upstream does — only T-implementing instances are ever linked into a given pool's list).event Action<T> ItemUsed/ItemReturned->System::MulticastAction<T*>(a true multicast delegate, unlikeEventHandler<TEventArgs>'s (sender, args) shape —MulticastAction<Args...>was added tosharp-runtimeearlier in this project specifically for this kind of need).Pool<T>/ObjectPool<T>'swhere T : classreference-type constraints map to working inT*throughout, consistent withGameComponentCollectionExtensions.hpp. Found and preserved a severe, genuine upstream bug inObjectPool<T>— an actual infinite loop, not just a wrong value, confirmed by hand-tracing the C# source byte-for-byte myself (not delegated, not assumed):CreateObject()unconditionally sets_tailNode = itemas its last step, immediately beforeNew()callsUse(item)on that exact same item;Use()'sif (_tailNode is null)check — meant to detect "is this the pool's first-ever node" — is therefore always false for every freshly-created item, soUse()always takes the branch that setsitem.PreviousNode = _tailNodeand_tailNode.NextNode = item— but since_tailNodeISitemat that point, the node ends up pointing to itself. For a pool's very first item, this meansGetEnumerator()'swhile (node != null) { yield return node; node = node.NextNode; }never terminates, becausenode.NextNode == node. The self-reference is only retroactively fixed by a laterCreateObject()call (forNextNode) or aReturn()call (which unconditionally resets the current tail'sNextNodeto null) — so enumerating a pool whose items were obtained purely viaNew(), with none everReturn()ed, hangs on the current tail. Reproduced exactly (not fixed), documented prominently inObjectPool.hpp's header comment, and covered by regression tests that deliberately assert ongetNextNodeProperty()/getPreviousNodeProperty()directly rather than by iterating the pool — iterating in the exact scenario being tested is the bug itself and would hang the test process. Smaller preserved quirk:Pool<T>::Free()callsresetItemunconditionally, even when the item wasn't actually kept because the pool was already at itsmaximum. Ported upstream's one activeObjectPoolTests.cstest 1:1 (its own name is stale/misleading — it says "ThrowsNullReferenceException" but actually asserts no exception is thrown); fresh tests for everything else including bothPool<T>(no upstream coverage at all) and theObjectPool<T>full/policy paths (ReturnNull/IncreaseSize/KillExisting) and the infinite-loop-bug regressions. One of my own test-authoring mistakes caught before landing: an initial test assumedCapacity's full-pool policy applies onceTotalCount > Capacitynaively at the secondNew()call, but upstream's actual guard isTotalCount <= Capacity(note<=), which allowsCapacity + 1total items to be freely created before the policy switch ever triggers — traced and corrected before committing, not just assumed correct. Test suite grew from 589 to 608 (19 net new tests). Both build modes verified clean, and the full suite explicitly re-run under a 60-secondtimeoutwrapper as an extra safety net given the infinite-loop bug under test, confirming no hang. - Collections:
KeyedCollection,DictionaryExtensions,ListExtensions(2026-07-13, ported directly, no fork; small, 106 lines of C# total across the 3 files).KeyedCollection<TKey, TValue>andListExtensions.Shufflefully ported as header-only templates.DictionaryExtensions.GetValueOrDefaultwas NOT ported — verified (not assumed) thatsharp-runtime's ownSystem::Collections::Generic::Dictionary<TKey, TValue>::GetValueOrDefault(key, defaultValue = TValue{})already implements byte-for-byte identical semantics to upstream's extension method (both: return the value if present, else a supplied or default-constructed fallback), unlike theRandomExtensions::NextSinglecase from task 21 where a same-namedsharp-runtimemethod turned out to implement a genuinely different algorithm and had to be re-ported instead of reused. Since the semantics here are a true match, this is a case where "reusesharp-runtime, don't re-roll" applies with no exception — adding a redundant free-function wrapper around an already-identical member method would be pure duplication with zero fidelity benefit, so this file is intentionally not ported; see the decisions log for the full account.KeyedCollection<TKey, TValue>is backed by that sameDictionary<TKey, TValue>(whoseoperator[](key) constalready throwsKeyNotFoundExceptionfor a missing key, exactly matching C#'sDictionaryindexer).CopyTomatches upstream: always throws (NotSupportedExceptionupstream).ListExtensions.Shuffle'sIList<T>parameter maps tostd::vector<T>&(matching every other C#IList<T>/array translation in this project) and correctly returns a reference to the same, now-shuffled vector, preserving upstream's fluent/chainable return. No upstream tests exist for any of the 3 files — wrote fresh tests forKeyedCollection/Shuffle(determinism, element-preservation, edge cases). Test suite grew from 608 to 624 (16 net new tests). Both build modes clean. - Collections:
ObservableCollection/IObservableCollection, Math:Ray2(2026-07-14, both ported directly, no fork; landed via the post-completion completeness audit's item-by-item owner review). Both were originally scoped out (ObservableCollectionin favor of reusingsharp-runtime's own real BCL equivalent, §2;Ray2never separately tracked at all — see below) but the project owner explicitly requested both be ported anyway during the audit review, for full fidelity even where a reasoned skip had already been made.ObservableCollection<T>/IObservableCollection<T>: a genuinely separate type fromsharp-runtime'sSystem::Collections::ObjectModel::ObservableCollection(different event shape — four separate events,ItemAdded/ItemRemoved/Clearing/Cleared, vs.sharp-runtime's singleCollectionChangedwith a change-description payload); both now coexist, pick whichever shape a call site needs. Built onsharp-runtime'sSystem::Collections::ObjectModel::Collection<T>(already exposes the exact protectedInsertItem/RemoveItem/ClearItems/SetItemvirtual hooks this type overrides).System::EventHandler<TEventArgs>:: Raiserequires aSystem::Object*sender, soObservableCollection<T>additionally inheritsSystem::Object(verified no diamond:Collection<T>'s own base chain never touchesObject) purely to have a validthisto pass — matchingFramesPerSecondCounter.hpp's establishedSystem::Object+EventHandlerpattern.GetTypeName()returns a fixed, non-templated string rather than attempting to embedT's name, matching the established "reflection-hazard, use a fixed sensible string" precedent (GameTimer's constructor-supplied name). Header-only template, matchingKeyedCollection<TKey,TValue>'s convention. No upstream tests exist; 10 fresh tests added (event-raising for add/remove/set/clear, interface conformance, base-Collection<T>passthrough,GetTypeName).Ray2(Math/Ray2.cs): discovered mid-audit to be a genuinely separate, older, minimal upstream type from the already-portedRay2D(different field name —PositionvsRay2D::Origin— and a much smaller surface, oneIntersects(BoundingRectangle, out, out)method vs.Ray2D's dozen overloads), not merely a naming variant as first assumed during the audit (a real correction made transparently, not silently). Confirmed viagrepacross upstreamsource/andtests/: zero call sites anywhere in upstream itself (one comment mention inPrimitivesHelper.cs) and no test coverage — legacy/superseded, but ported anyway per the owner's explicit request.IEquatableByRef<Ray2>intentionally not inherited, matching the identical precedent already set by every sibling geometric value type in this project (BoundingBox2D,Line2D,LineSegment2D,Ray2D, etc. — none of them inherit it either, despite upstream implementing it). 7 fresh tests added (construction, equality, hashing,ToString, 3Intersectsscenarios viaPrimitivesHelper::IntersectsSlab, already ported and reused as-is). Both build configs verified clean via genuinerm -rf-based rebuilds; fullctest— 2063/2063 passing (was 2046). - Port
tests/MonoGame.Extended.Tests/{Math,Primitives,Shapes,Collections}as GoogleTest suites (2026-07-13) — audited personally first: all 16 upstream files under the 4 literal named folders were already fully covered by tests ported during each type's own earlier implementation task, confirmed by reading every one and cross-checking against the correspondingcna-extendedtest file (not just trusting a stale assumption). Widened the audit past the plan's literal 4-folder wording to root-level upstream test files (tests/MonoGame.Extended.Tests/*.cs, not in a subfolder) testing Phase-1-scope types, since this task's actual intent — full Phase-1 test parity — is broader than its literal wording. That wider sweep found a real, substantial gap:BoundingBox2D/BoundingCapsule2D/BoundingCircle2D/BoundingPolygon2D/OrientedBoundingBox2D/LineSegment2Deach had noticeably fewer tests ported than upstream's own root-level test files for those types actually have (OrientedBoundingBox2Dwas the largest gap: 13 ported vs. 33 active upstream). Delegated closing this gap to a forked sub-agent, given the volume (7 files audited for gaps, 8 more given a lighter verification pass). Fork's findings, independently verified before landing (re-read 2 of the 6 modified files' diffs against the actual upstream.cssource line-by-line, including the most calculation-heavy new tests likeOrientedBoundingBox2D'sCreateFromRotation90Degrees/TransformNonUniformScale— all matched exactly): +43 tests total acrossBoundingBox2D(+4),BoundingCapsule2D(+5),BoundingCircle2D(+8),BoundingPolygon2D(+11),LineSegment2D(+6),OrientedBoundingBox2D(+9);HslColorhad no real gap (the count mismatch was a[Theory]-vs-TEST_Pparameterized-test counting artifact, not a coverage gap). Confirmed (against each type's own header-comment-documented deferred-vs-ported split, not assumed from test names) that every remaining un-ported upstream test in these 6 files genuinely depends on the not-yet-portedCollision2Dsystem (TryGetCollision_*,Contains/ContainsPointwhereContainsitself is deferred,Distance*/IntersectsonLineSegment2D) or has no C++-translatable concept (GetCorners_ThrowsWhenArrayNull— this project'sstd::vector<T>&out-param convention has no "null" case). No genuine discrepancies found — no case where a header comment claimed something was ported but the functionality didn't actually exist. The lighter verification pass on 8 more root-level files (Angle,ColorExtensions,ColorHelper,Line2D,MathExtended,Ray2D,RectangleExtensions,Vector2Extensions) confirmed all already at parity or better (extra fresh coverage beyond upstream).OrthographicCameraTests.cs(53 active upstream tests) intentionally excluded —OrthographicCameraitself remains correctly deferred to Phase 3 pendingViewportAdapters, not a Phase 1 gap. Test suite grew from 624 to 663 (39 net new tests). Both build configurations verified clean from a genuinely clean rebuild, independently re-confirmed by me (not just trusting the fork's self-report), including the headers-only compile-check config. This was the last Phase 1 task — Phase 1 is now complete.
Depends on Phase 1 (math/shapes).
- Root types:
Collision2D,CollisionResult2D,CollisionShape2D,CollisionShapeKind2D— COMPLETE (2026-07-13).CollisionShapeKind2D,CollisionResult2D, andCollision2D(3,809 upstream lines) are all fully ported AND at full test parity:Collision2D's self-check diff confirms all 79public staticmethods present (zero missing/extra). The Collision2D-dependent follow-up sweep is complete:Ray2D(100%),Line2D(100%),LineSegment2D(100%, includingIntersects(BoundingPolygon2D)), and all 5 bounding-volume types' ownContains/Intersects/TryGetCollisionoverloads —BoundingBox2D,BoundingCircle2D,OrientedBoundingBox2D,BoundingCapsule2D(also landingCreateFromSegment/CreateMerged),BoundingPolygon2D(also landingContains(Vector2), which unblockedLineSegment2D's last method). Each type's asymmetric upstream Contains/Intersects/TryGetCollision coverage (not every shape pair has all three) was verified against upstream, not assumed. Finally,CollisionShape2D(713 lines, a tagged-union type dispatching Intersects/ TryGetCollision via nested switches to the 5 bounding-volume types' instance methods) is ported, including itsinternal TryGetLegacyPenetrationVectorAPI and faithfully preserving its own asymmetric dispatch coverage (e.g.Intersects(Ray2D, out, out)/Intersects(LineSegment2D, out, out)both exclude the Polygon kind;TryGetCollisiononly handles 15 of 25 kind pairs) — ported directly rather than via fork, given it's one cohesive file. No upstream test file exists for it; 26 fresh tests added (spot-check pair per dispatch branch). 1045 tests passing total for the whole project. SeeNEXT.mdentries (19)-(24) for the full breakdown. -
CollisionWorld2D,ICollisionActor,ICollisionBroadphase2D,CollisionEvent2D,CollisionPair2D,ActorPairKey— COMPLETE (2026-07-13). All 6 types ported (newCNA::Extended::Collisionssub-namespace, matching upstream's ownMonoGame.Extended.Collisionsnamespace split from the root types).CollisionWorld2D(504 lines) was internally deferred until task 3/4 landed — seeNEXT.mdentries (25) and (28) for the full breakdown, including theDuplicateNameException→ArgumentExceptionsubstitution (no sharp-runtime equivalent exists) and the null/empty-string layerName sentinel decision. - Broadphase:
QuadTree/*,SpatialHash— COMPLETE (2026-07-13).QuadtreeData,QuadTree,QuadTreeSpace, andSpatialHash(both implementingICollisionBroadphase2D) all ported, with 44 tests ported 1:1 from upstream. SeeNEXT.mdentry (26). -
Layers/*,LayerPair— COMPLETE (2026-07-13).Layer,LayerPair,UndefinedLayerExceptionall ported. 12 fresh tests (no upstream test files exist for any of these 3 types). SeeNEXT.mdentry (27). - Port
tests/MonoGame.Extended.Tests/Collisions/**— COMPLETE (2026-07-13). All 6 upstream test files ported (CollisionEvent2DTests.cs,CollisionWorld2DTests.cs,QuadTreeSpaceTests.cs,QuadTreeTests.cs,SpatialHashTests.cs, plus the sharedImplementation/BasicActor.csfixture), landed incrementally alongside each piece above per this project's "port tests alongside implementation" rule. Fresh tests added where no upstream file exists (ActorPairKeyTests,CollisionPair2DTests,LayerTests,LayerPairTests,UndefinedLayerExceptionTests). Phase 2 is now fully complete — 1138 tests passing total for the whole project.
Independent of each other; depends only on Phase 1 and CNA's Microsoft::Xna::Framework::Input.
-
Input/listeners (keyboard/mouse/gamepad/touch) and their event-args/settings types — 24 upstream files,CNA::Extended::Input/::InputListenersnamespace. 32 fresh tests (no upstream test files exist). Cross-depends onViewportAdapters(forward-declared, non-owning pointer). SeeNEXT.mdentry (29). -
Timers/*—GameTimer,ContinuousClock,CountdownTimer,TimerState. SameUpdateOrder/EnabledChangedcopy-paste bug asFramesPerSecondCounter(Phase 1), preserved and documented. 11 fresh tests. Ported directly (small). -
Tweening/*(easing functions +Tweener) — genuine C++ redesign, user-approved: upstream's reflection/expression-tree-based member access (x => x.Property) has no C++ equivalent; replaced with pointer-to-member syntax (tweener.TweenTo(&target, &T::Member, ...)). SeeNEXT.mdentry (29) for the full design rationale (TypedTweenrenaming,LinearOperations<T>elimination, etc.). 6 tests ported from upstream. -
ViewportAdapters/*(BoxingViewportAdapter,ScalingViewportAdapter,DefaultViewportAdapter, etc.) — all 5 types ported directly (small). One likely upstream bug preserved (BoxingViewportAdapter's Letterbox condition compares width against the wrong dimension). No fresh tests — upstream's own 2 test files are entirely commented-out dead code, and every method needs a liveGraphicsDevice&. -
VectorDraw/*(debug line/shape drawing) —PrimitiveBatch/PrimitiveDrawing, full 1:1 port, no CNA graphics-API mismatches found. No upstream tests exist and no GPU-free logic surface to test in isolation. - Port
tests/MonoGame.Extended.Tests/{ViewportAdapters,Tweening}—Tweening's test file ported (adapted to the new calling convention);ViewportAdapters' 2 test files are dead code upstream, nothing to port. 49 total fresh/ported tests across Phase 3. 1187 tests passing total for the whole project.
Depends on Phase 1; needs CNA's GameComponent/Game.
-
Screens/*(Screen,ScreenManager, transitions, etc.) —Screen,GameScreen,ScreenManager, and the abstractTransitionbase are ported. See the earlier session entry for the full design rationale (non-owningScreen*, the transition-completion deferred-destruction fix, etc.).FadeTransition/ExpandTransition— COMPLETE (2026-07-13), unblocked once Phase 5'sShapeExtensions/FillRectanglelanded. Both are thin wrappers around a value-ownedSpriteBatchmember (constructed in the initializer list — no heap allocation needed, unlikeBitmapFont's page-texture vector, since nothing else aliases thisSpriteBatch) plusCNA::Extended::FillRectangle. Upstream'snullblendStateargument toSpriteBatch.Begin(...)(meaning "use the default") is translated as an explicitBlendState::AlphaBlend, confirmed to be the actual default by reading CNA's own parameterlessSpriteBatch::Begin()overload's implementation. No upstream tests exist forTransition/FadeTransition/ExpandTransitionat all (confirmed via search) — matches the already-documented "no live-GraphicsDevice test infra" gap for anything constructing a realSpriteBatch. - Port
tests/MonoGame.Extended.Tests/Screens—ScreenManagerTests.cs/GameScreenTests.csported (28+1 tests, 4 C#-cache-identity-specific tests replaced with a content-correctness equivalent), plus 3 fresh tests for the Transition-based overloads. Nothing to port yet forFadeTransition/ExpandTransitionsince they're deferred. 1218 tests passing total for the whole project.
Depends on Phase 1 and CNA's GraphicsDevice/SpriteBatch/Effect/Texture2D.
-
Graphics/Effects/*(customEffectwrapper) — COMPLETE (2026-07-13). Confirmed the flagged design question was real: CNA'sEffect(GraphicsDevice&, bytecode)constructor always throwsNotImplementedException(no MonoGame.mgfxobytecode parser yet). Asked the user; re-authoredDefaultEffect/MatrixChainEffecton CNA'sShaderEffect(hand-written GLSL) instead of a literal port — upstream's 4 compile-time techniques collapsed into 1 GLSL program with 2 runtime uniform toggles. Verified with a real GPU render (not just a C++ compile) via a one-time scratch program modeled on CNA's owneasygl_shader_effect_test.cpp— passed on real OpenGL ES 3.2 (Mesa) hardware.EffectResource.csnot ported (superseded entirely).ITextureEffect,IMatrixChainEffectalso landed. SeeNEXT.mdentry (31) for the full rationale. -
Sprite,AnimatedSprite,SpriteSheet,SpriteSheetAnimation,SpriteSheetAnimationBuilder,SpriteSheetAnimationFrame— COMPLETE (2026-07-13). Ported as one coordinated dependency-ordered unit (seeNEXT.md).AnimatedSpriteconsumes the parallel-landedAnimations/module'sIAnimationController/AnimationController(see below).SpriteSheetAnimationFrame's upstreaminternalconstructor kept public per this port's established internal-visibility convention. -
Texture2DAtlas,Texture2DRegion,Texture2DRegion.Extensions— COMPLETE (2026-07-13).Texture2DRegionisstd::shared_ptr-managed throughout (a genuine multiply-aliased reference type upstream: atlas indexes it twice,Sprite/NinePatchhold it,GetSubregionhands out fresh ones) — seeTexture2DRegion.hpp's header comment for the full ownership reasoning.Texture2DAtlas'sinternal static CalculateRegionsexposed (not keptinternal-equivalent-private) specifically so upstream's own issue #1013 regression test could be ported verbatim. Theinternal GetRegions(ReadOnlySpan<IAnimationFrame> frames)overload is not ported — confirmed via grep to have zero call sites anywhere in MonoGame.Extended itself; add if a real caller ever needs it. -
NinePatch— COMPLETE (2026-07-13). -
SpriteBatch.Extensions,GraphicsDevice.Extensions,RenderTarget2DExtensions,PrimitiveTypeExtensions,FlipFlags(IMatrixChainEffectalready landed above) — COMPLETE (2026-07-13).SpriteBatch.Extensions' private_patchCachescratch buffer ported as a translation-unit-local static array; its dead_rectfield was not ported (confirmed unused anywhere in the file). -
Content/TexturePacker/*(direct-JSON TexturePacker atlas format — not xnb-based) — COMPLETE (2026-07-13). Upstream'srecords with[property: JsonPropertyName(...)]ported as get-onlygetXProperty()classes, each with a hand-writtenfrom_jsonfree function (nlohmann's ADL customization point, perJsonSerializer.hpp's own documented reflection replacement) supplying the custom JSON-key mappingJsonPropertyNamegives in C#.TexturePackerTexture::Size's upstream default (= defaulton a reference-typerecord, i.e. genuinely nullable at runtime despite its non-nullable-looking C# type) is ported asstd::optional<TexturePackerSize>, not a plain value, matching the real runtime behavior rather than the misleading static type. No upstream unit tests exist for this module; 7 fresh tests added. -
Content/ExternalResourceResolver(s)(not xnb-specific) — COMPLETE, landed alongside the other small independent Graphics/Content utilities in an earlier Phase 5 commit; checkbox was simply missed at the time. -
BitmapFonts/*(runtimeBitmapFont/BitmapFontCharacter/BitmapFont.Extensions) andContent/BitmapFonts/{BitmapFontFileContent,BitmapFontFileReader}— COMPLETE (2026-07-13). Scope correction: the exclusion list above previously calledContent/BitmapFonts/"the xnb-side helper," which was wrong. Read both files directly:BitmapFontFileReader.csis a direct parser for the AngleCode BMFont.fntspec (binary/text/XML variants, confirmed via its own upstream test fixtures undertests/MonoGame.Extended.Tests/BitmapFonts/files/bmfont/*.fnt) — zero xnb/ContentReader/ ContentManager dependency, exactly analogous to the already-in-scope direct-JSON TexturePacker reader above. It is the loader for the already-approved-in-scope "BMFont bitmap fonts" goal (see the in-scope bullet list above), not a xnb helper for it. The exclusion-list entry further up this file is corrected accordingly. Realsharp-runtimebug found and worked around, not fixed there (sibling-repo rule):System::Xml::XmlNode::SelectSingleNode's doc comment claims "caller takes ownership," but it returns a pointer into the live DOM tree (verified by tracingXmlNode.cpp→XPath::XmlDocumentNavigator::GetNode());BitmapFontFileReader.cpptreats the return value as non-owning instead. Realcna-extendedCMake bug found and fixed here (in scope, not a sibling repo): the headers-only build branch never addedsharp-runtime/vendorto the include path, so any header pulling in a vendored dependency (tinyxml2/tinyxml2.hfor the new XML reader;nlohmann/json.hpphad been silently working only because this dev machine happens to have a systemnlohmann-json3-devpackage) failed to compile in that config — fixed inCMakeLists.txt, verified with a genuinely clean headers-only rebuild. Independently line-reviewed the runtimeBitmapFont.cppglyph-enumeration/FromFile/FromStreamlogic against upstream — exact match, including two genuine, deliberately-preserved upstream inconsistencies (line-height increment omitsLineSpacingfor thestringoverload but not theStringBuilderone; newline resetspositionDelta.Xto0for thestringiterator but toposition.Xfor theStringBuilderone). -
Animations/*(AnimationController,AnimationEvent,AnimationEventTrigger,IAnimationController,IAnimation,IAnimationFrame) + rootAnimationComponent— COMPLETE (2026-07-13), ported in parallel with the Sprite/Texture2D chain above (confirmed independent via grep before parallelizing: zero references from Animations/* into the Sprite cluster at the time). Full file-by-file member audit against upstream now done (deferred at landing time, completed this session): all 7 files (IAnimation,IAnimationFrame,IAnimationController,AnimationController,AnimationEvent,AnimationEventTrigger,AnimationComponent) checked member-by-member against their upstream.cscounterparts — exact match throughout, including the correctly-preserved observation thatAnimationEventis itself unused within the module (upstream raisesOnAnimationEventas a rawAction, never constructs anAnimationEventinstance) andAnimationComponent's reverse-iteration + prune-disposedUpdate()pattern. -
Math/ShapeExtensions.cs(DrawPolygon/FillRectangle/DrawRectangle/DrawLine/DrawPoint/DrawCircle/DrawEllipse/DrawArcSpriteBatchdebug-drawing extensions) — COMPLETE (2026-07-13). Despite living under theMath/folder upstream, its declared namespace is the C# rootMonoGame.Extended(not.Graphics) — ported into the matching rootCNA::Extendednamespace, notCNA::Extended::Graphics, per this project's "namespace follows the actual C# declaration" convention.IReadOnlyList <Vector2>ported asconst std::vector<Vector2>&. This unblocked Phase 4's deferredFadeTransition/ExpandTransition(see that phase's entry above). No upstream tests exist for this file. - Port
tests/MonoGame.Extended.Tests/{Graphics,BitmapFonts,Animations}— COMPLETE.Texture2DAtlasTests.cs(Graphics) andBitmapFontTests.cs/BitmapFontFileReaderTests.cs(BitmapFonts) ported as their own files;Animations/AnimationTests.csturned out to already be fully ported (all 15[Fact]tests present 1:1 intests/CNA/Extended/Animations/AnimationControllerTests.cpp, asTEST_Ffixtures rather than flatTESTs — a deliberate, documented GoogleTest idiom choice, not a gap) — this checkbox was simply never marked done despite the work existing; corrected this session after re-reading the file and confirming all 15 tests match upstream one-to-one.
Depends on Phase 1. Feeds Phase 7 (Tilemaps).
-
Serialization/*(JSON converters) — COMPLETE. Reusessharp-runtime'sSystem::Text::Json(nlohmann::ordered_jsonunder the hood) rather than hand-rolling a parallel JSON layer, per this phase's own scoping note. Design decision: upstream'sJsonConverter<T>classes are opted into per-call viaJsonSerializerOptions.Converters.Add(...);sharp-runtime'sJsonSerializerhas no such per-call registration, dispatching only through nlohmann's global ADL customization points. Stateless value-type converters (Color,HslColor,Vector2,RectangleF,Thickness,SizeF,Interval<T>) becamenlohmann::adl_serializer<T>full/partial specializations — nlohmann's own documented mechanism for foreign types this project doesn't own the definition of. Genuinely stateful converters needing injected state (TextureRegion2DJsonConverter,NinePatchJsonConverter,TextureAtlasJsonConverter,ContentManagerJsonConverter<T>) became explicit classes the caller constructs and invokes directly, since ADL hooks have no room for injected dependencies.MonoGameJsonSerializerOptionsProvideris kept only for formatting options; its C# purpose (assembling aConverterslist) is moot since the stateless converters are always ADL-active.BaseTypeJsonConverter.csexplicitly not ported: reflection-heavy (System.Reflection/TypeInfo/runtime-Type-keyed dispatch) with zero call sites anywhere in upstream (confirmed via grep across bothsource/andtests/) — matches the established "skip zero-call-site reflection-dependent code, document, add later if needed" pattern. Addedinclude/CNA/Extended/Content/ContentManagerExtensions.hpp, a small in-scope dependency (Load<T>(ContentManager&, path, loader)) needed byJsonContentLoader/TextureAtlasJsonConverter— uses the normal runtimeContentManager, not the excluded xnb-registrationExtendedContentManager. Two real bugs found during independent verification and fixed (not by the fork — caught via a genuinely clean rebuild + fullctestrun before committing): (1)XmlReaderExtensions.cpp's numeric attribute parsers usedstd::stoi/std::stof, which only parse a leading numeric prefix and silently ignore trailing garbage (e.g.std::stoi("1.5") == 1, no exception) — unlike upstream'sint.Parse/float.Parse, which require the whole string to be valid and throwFormatExceptionotherwise. Fixed by switching toSystem::Int32::Parse/System::Single::Parse(already available insharp-runtime, matching that exact strictness) throughout the file — caught by the portedGetAttributeRectangleInvalidFormatThrowstest failing for"1.5,2,3,4". (2)FloatStringConverter.cpphad the samestd::stofleniency issue and didn't match upstream'sfloat.TryParse-then-fallthrough structure; fixed withSystem::Single::TryParse. Preserved, not fixed, a genuine upstream bug:TextureAtlasJsonConverter's JSON-string read branch is dead/broken in upstream itself (its own source has a// TODO ... Need to investigatecomment above unreachable code and an unconditionalthrow new NotImplementedException()) — kept as-is. A real dangling-pointer bug from an early draft (aTexture2Dloaded into a local, pointed to by the returnedTexture2DAtlas, with no natural owner sinceContentManager::Load<T>()returns by value) was caught and fixed by giving the converter its ownstd::vector<std::unique_ptr<Texture2D>>, matchingBitmapFont's established precedent for the same problem. -
Serialization/Xml/*— COMPLETE, usingsharp-runtime's realSystem::XmlDOM (XmlDocument/XmlNode/XmlReader/XmlWriter). - Port
tests/MonoGame.Extended.Tests/Serialization/**— COMPLETE. The 4 upstream test files (ColorJsonConverterTests,RectangleFJsonConverterTest,Xml/XmlWriterExtensionsTests,Xml/XmlReaderExtensionsTests) ported, plus fresh coverage for converters with no upstream test (Vector2/Thickness/Size2/HslColor/Interval/XmlNodeExtensions). One fresh test (HslColorJsonConverterTests.RoundTripPreservesRgbColor, exact-equality) was itself too strict and had to be corrected to an approximate comparison after independent verification:HslColor::FromRgb/ToRgb(pre-existing, independently tested) are not exact bitwise inverses of each other for arbitrary RGB values — confirmed directly, without any JSON involved, that a plain round trip ofColor(10,20,30,255)already drifts to(9,20,30,255), and a further round trip drifts again rather than stabilizing. Renamed toRoundTripPreservesRgbColorApproximatelywith a documented tolerance. - Correction (2026-07-14, found via a user-prompted full-plan completeness
audit — see below):
Serialization/Json/SizeJsonConverter.cs(the JSON converter for the integerSizetype — distinct fromSize2JsonConverter.cs, which convertsSizeFand was already ported) was genuinely missed during Phase 6 — not documented as excluded anywhere, just absent. Now ported asSizeJsonConverter.hpp/.cpp, followingSize2JsonConverter's exact established pattern (nlohmann::adl_serializer<Size>, reusingReadAsMultiDimensional<int>). No upstream tests exist for it either (same asSize2JsonConverter); 4 fresh tests added mirroringSize2JsonConverterTests.cpp's structure. This is the only genuine gap found across the entire ~388-file upstream source tree in that audit — see the new "Post-completion completeness audit" note below for the full account of what was checked and why everything else that looked like a gap on a first pass (raw file-count mismatches per module) turned out to be legitimate (consolidated files, documented exclusions, or naming differences).
Post-completion completeness audit (2026-07-14): after Phase 10 was already
marked complete, the project owner spot-checked for LinearOperations (a
reflection-based C# arithmetic-delegate class, Tweening/LinearOperations.cs) and
didn't find it, worrying the port might have systematic gaps. Investigation
confirmed LinearOperations<T> itself was legitimately, deliberately eliminated
(documented in LinearTween.hpp's own header comment: C++ templates use
operator+/operator-/operator* directly, a strictly better compile-time
equivalent of what upstream's runtime expression-tree compilation worked around) —
but the underlying worry deserved a real answer, not just clearing that one
spot-check. Did a full reconciliation of all 388 upstream .cs files (excluding
tests/) against the ported tree, module by module: counted files per top-level
upstream directory, compared against the ported directory's file count, and for
every directory with a raw-count mismatch, checked each individual missing
filename by hand against plan.md's decision log or by reading the upstream file
directly. Result: every single mismatch across every module (Collections,
Content, ECS, Graphics, Input, Serialization, Tilemaps/Tiled/LDtk/Ogmo, Tweening,
all of Math/, all 35 root-level files) was one of: (a) an already-documented,
reasoned exclusion in plan.md (ComponentType.cs, BaseTypeJsonConverter.cs,
EffectResource.cs, KeyboardState.Extensions.cs's internal-only FNA shim
methods, TiledDataDecoder.cs/TiledPropertyConverter.cs, the xnb
ContentReaders/Content/*Reader family); (b) a legitimate multi-file-to-one-file
consolidation, each with its own explaining header comment (Tiled/LDtk/Ogmo's ~65
combined Document-DTO .cs files each folded into one *Document.hpp per format;
OgmoLayerDataConverter.cs/OgmoTileCoordConverter.cs's polymorphic-dispatch and
custom-coordinate-parsing logic folded into ParseOgmoLayerData/ParseCoordPair
free functions rather than kept as separate JsonConverter<T> classes;
TweenFieldMember.cs/TweenPropertyMember.cs/TweenMember.cs's reflection-based
3-class hierarchy replaced by one pointer-to-member-based TweenMember<TTarget, TMember>, an explicit user-approved redesign); or (c) SizeJsonConverter.cs — the
one real, undocumented gap, fixed above. No other gaps found in the initial pass.
Owner review of every finding (2026-07-14, same day): rather than take the above conclusions on trust, the owner asked to walk through all 14 items one by one. Two corrections came out of that review, both landed (see the Phase 1 Collections/Math checklist entry above for what was actually built):
ObservableCollection/IObservableCollection: the owner overturned the original "reusesharp-runtime's equivalent instead" decision (§2) and asked for MonoGame.Extended's own type to be ported anyway, as a genuinely separate type. Done.Ray2.cs→Ray2D.hpp, originally reported above as "a trivial naming difference" — this was WRONG, caught and corrected transparently, not silently, during the walkthrough.Ray2D.hppis in fact a correct, faithful port of a different, real upstream file (Ray2D.cs, root-level, fieldOrigin, a dozenIntersectsoverloads) — genuinely distinct fromMath/Ray2.cs(fieldPosition, oneIntersects(BoundingRectangle,...)method), which this audit had incorrectly conflated withRay2D.cspurely because of the similar name.Ray2itself is real, upstream, zero-call-site dead code (confirmed bygrep, same category asComponentType.cs/BaseTypeJsonConverter.cs) — but the owner asked for it to be ported anyway, for full fidelity. Done, as a genuinely separate type alongsideRay2D(no rename of anything).
Both build configs verified clean and the full suite (2063/2063) passing after both corrections.
The largest module (121 + 22 files). Depends on Phases 1, 5 (rendering), 6 (serialization) — all now complete.
- Core:
Tilemap,TilemapData,TilemapFactory,TilemapLayerCollection,TilemapOrientation,TilemapTile*,TilemapTileset*,TilemapWorld,TilemapLayers/*(all 7 files),TilemapObjects/*(all 8 files),Properties/*(all 3 files),Parsers/*(both files) — COMPLETE (2026-07-13) (format-agnostic data model + factory, ~40 files/~5900 lines). Ownership model, derived from upstream's actual field types and real construction call sites inTilemapFactory.cs(not guessed):TilemapLayerCollectionowns its layers;TilemapTileLayerowns its own tile grid (flat array, independent per layer — tiles are never shared between layers);TilemapObjectLayerowns its placed objects;TilemapImageLayerholds a non-owningTexture2D*(external-GPU-resource convention, already established).TilemapTilesetCollectionowns its tilesets — this required a correction mid-task: an initial non-owning design (based onTilemapTilesetCollection.csalone, which gives no ownership signal) was fixed after reading the realTilemapFactory.csconstruction call site (tilemap.Tilesets.Add(tileset), a freshly-built local with no other reference — independently confirmed by re-reading that exact line).TilemapTilesetowns its tile data;TilemapTile(a small value type, global ID + flip flags) owns nothing and resolves its data by lookup through aTilemapTilesetCollection&at call time — this lookup-not-reference pattern is the actual sharing mechanism: many tile placements across many layers share oneTilemapTileDatainstance without duplicating it.TilemapPropertyValueported as a literal C++union(int/float/bool/packed-color) + astd::stringkept outside the union + a type-tag enum — matching upstream's own[StructLayout(LayoutKind.Explicit)]real intentional memory layout exactly.std::variantwas considered and rejected: it wouldn't reproduce upstream's actual chosen representation and buys nothing for the required throw-on-wrong-type-accessAs*()accessors. Real dangling-pointer bug found and fixed during porting (self-caught, matchingBitmapFont::pageTextures_'s already-established precedent for the identical problem):TilemapFactory::Build's first draft loaded textures into a function-local owning vector destroyed on return, leaving every texture pointer in the returnedTilemapdangling — fixed by givingTilemapits ownownedTextures_(vector<unique_ptr<Texture2D>>) via a newAddOwnedTexturemethod (no upstream equivalent needed, since C#'s GC keeps each loaded texture alive via whatever tileset/layer field references it). 8 upstream test files ported 1:1. Process note: the fork producing this work editedplan.mdtwice despite an explicit standing instruction not to (once before any correction, once again in the very same turn after being told not to and acknowledging it) — both edits were uncommitted and discarded (git checkout -- plan.md) before ever being committed; the actual ported code was independently verified separately and is unaffected. SeeNEXT.mdfor the full incident note. -
RenderMode,TilemapRendererShared,TilemapSpriteBatchRenderer,TilemapWorldSpriteBatchRenderer— COMPLETE (2026-07-13).TilemapRenderer/TilemapWorldRenderer— COMPLETE (2026-07-14), previouslyneeds_human, now resolved. Root cause of the blocker as originally documented: these two draw via rawVertexBuffer/IndexBuffer+VertexPositionColorTexture, notSpriteBatch, and this project's ownDefaultEffect(Phase 5) was correctly, deliberately hand-authored with a GLSL vertex layout matchingSpriteBatch's own internal compact vertex format (vec2 aPos@0,vec2 aTexCoord@1,vec4 aColor@2), structurally incompatible withVertexPositionColorTexture(Vector3 Position,Color,Vector2 TextureCoordinate). Of the 3 originally-listed options (newDefaultEffect-shaped Effect, changeDefaultEffect's shader, or a custom vertex struct), none were taken. The user's direct question — "isn'tDefaultEffecta known XNA 4.0 class, and isn't it already ported incnaper the real XNA API?" — prompted the actual resolution:DefaultEffectis not an XNA class at all (it's MonoGame.Extended's own invention,MonoGame.Extended.Graphics.Effectsnamespace, built from its own precompiled.mgfxobytecode with 4 baked-in techniques); the class the user was actually thinking of, XNA's realBasicEffect, already exists incna, fully implemented and pixel-verified across all 3 backends (EasyGL/ Vulkan/Bgfx — seecna/docs/basiceffect-support.md), including specifically theTextureEnabled=true+VertexColorEnabled=true+LightingEnabled=falsecombination over the exactVertexPositionColorTexturestride-24 layout these two renderers use.BasicEffect's feature set is a strict superset ofDefaultEffect's (which never had lighting/fog/specular at all, and never exposed a settableDiffuseColor— onlyAlpha), so no capability is lost by using it instead. Ported via a forked sub-agent (~2000 lines C# →TilemapRenderer.hpp/.cpp1728 lines,TilemapWorldRenderer.hpp/.cpp459 lines, plus 51 new tests), independently verified by the orchestrating session (cleangit status, clean rebuild of both configs from scratch, fullctestre-run, line-by-line source comparison of several core methods — e.g.Update/BuildLayerModels— against the upstream.cs, confirmed exact logical fidelity including preserved comments). Both classes' internalBasicEffectis configuredTextureEnabled=true,VertexColorEnabled=true,LightingEnabledleft at its defaultfalse, matchingDefaultEffect's always-on texture+vertex-color/no-lighting behavior exactly. Draw mechanics (SetVertexBuffer/SetIndexBuffer→effect.Apply()→DrawIndexedPrimitives) re-author upstream's XNA dynamic-reflectionParameters["Texture"]/CurrentTechnique.Passeswalk in CNA's idiomatic style, matching the precedentDefaultEffect.cppitself already established. The publicEffectproperty is narrowed from upstream's genericEffecttoBasicEffect&(CNA'sEffectbase has no generic parameter-reflection surface for an arbitrary caller-supplied effect to plug into) — the one real, deliberate API-shape deviation beyond theDefaultEffect→BasicEffectsubstitution itself, documented in both headers' top comments. Full details, all other minor translation deviations (Dictionary/HashSet→std::unordered_map/std::unordered_setfor move-only mapped types, matchingTilemapTileset.hpp's precedent, etc.), and verification commands are inNEXT.md. This was the sole remaining architectural blocker in the entire porting plan outside of Phase 10. The trailing test-coverage item (see theTilemapIntegrationTests.csentry immediately below) has since also landed — Phase 7 is fully complete and only Phase 10 remains in the whole plan. Real gap-fill needed and delivered as a prerequisite:OrthographicCamera(see its own corrected Phase 1 entry above) had been deferred since Phase 3 and never actually landed — ported here since everyRendering/*class needs it. Real bug found incna(sibling repo, not touched):BoundingFrustum::Contains (const Vector3&, ContainmentType&)has an extraclassifyPoint == 0.0f → Intersectsbranch with no upstream MonoGame equivalent (real MonoGame's version is a strict Disjoint/Contains binary) — confirmed directly incna/src/.../BoundingFrustum.cpp. Makes a point exactly on a clip plane wrongly reportIntersects. TwoOrthographicCameraTestshit this for real;GTEST_SKIP()-guarded with a full root-cause writeup in the test file, not deleted or weakened. Significant correction to this project's own documentation, independently re-verified before accepting it: the "no headlessSpriteBatch/GraphicsDevicetest infra exists" claim repeated throughout this session'sNEXT.md(forShapeExtensions,SpriteBatchExtensions,BitmapFontExtensions,FadeTransition/ExpandTransition) turns out to have been an untested assumption, not a verified fact — a bareGraphicsDevice graphicsDevice;+SpriteBatch spriteBatch(graphicsDevice);+Texture2D(graphicsDevice, w, h)genuinely construct and render end-to-end in this environment (real EasyGL-over-Mesa software rendering, confirmed by this phase's own new tests actually callingBegin/Draw/Endand passing). Nobody had actually tried it until this task did. This means the other modules listed above are very likely real-test-coverable too, not just compile-checkable — worth revisiting; seeNEXT.md. Follow-up done the same session: real behavioral test coverage added for all four previously-"compile-checked-only" modules (ShapeExtensions— 21 tests covering all 8 shape functions;SpriteBatchExtensions— 9 tests coveringNinePatch/Sprite/Texture2D/Texture2DRegion;BitmapFontExtensions— 9 tests covering all 6string/StringBuilderDrawStringoverload pairs;FadeTransition/ExpandTransition— 4 tests each, driven through realUpdate()calls to exerciseDraw()pastValue == 0) — all genuinely exercisingBegin/Draw/Endagainst a real headlessGraphicsDevice, not mocks. Found and fixed a real, previously undetected bug in this process:FadeTransition/ExpandTransition(landed earlier this session) never overrodeSystem::Object::GetTypeName()(pure virtual, independently confirmed viagreponObject.hpp), making both classes genuinely non-instantiable the whole time since landing — invisible because nothing had ever actually tried to construct one (no prior test did). Fixed by adding the override, matchingAnimationComponent::GetTypeName()'s established convention. 1888 → 1935 tests (+47), zero regressions. -
Tiled/*(TMX/JSON parser —TiledTmxParserand friends) — COMPLETE (2026-07-13), priority given the user's existingtiled-blupiproject. Preserves upstream's two-stage design (raw XML →TiledMapXmldocument model →TilemapData), sinceSystem.Xml.Serialization.XmlSerializerhas no C++ equivalent — hand-written DOM walk viaSystem::Xml'sXmlDocument/XmlElementinstead. Tile-layer data decoding (CSV, base64 uncompressed/gzip/zlib) usessharp-runtime'sConvert::FromBase64String+GZipStream/DeflateStream(zlib handled manually: strip the 2-byte header + 4-byte Adler32 trailer, feed the raw deflate stream toDeflateStream) — zstd explicitly rejected with a clear error, matching upstream. Decoded gzip/zlib fixture payloads were independently verified against Python's owngzip/zlibmodules before writing test assertions.TiledDataDecoder.cs/TiledPropertyConverter.csexplicitly not ported — confirmed via grep to have zero call sites anywhere upstream (source or tests);TiledTilemapDataConverter.cshas its own independent, actually-used equivalent logic. Real bug found and fixed in this port's own new code (notsharp-runtime): anXmlNode::getChildNodesProperty()non-owning cached pointer was initially double-wrapped in a secondunique_ptrby this port's own child-element helpers, causing a double-free segfault on any<map>with element children — fixed by using raw observer pointers. Realsharp-runtimebug found and worked around, not fixed (sibling-repo rule):System/Convert.hpp'susing SharpRuntime::Single;collides withSystem/Single.hpp'sclass Singlewhen both headers land in one translation unit — worked around locally withstd::from_chars-based strict parsing helpers instead of usingSystem::Singledirectly in the converter. 33 fresh tests (6TiledColorParser+ 27TiledTmxParser; no upstream unit tests existed to port 1:1 for the color parser, and the upstreamTiledTmxParserTests.cswas re-derived against fixtures rather than copied verbatim — checkNEXT.md/the commit for detail if exact upstream-test parity here ever matters). Process note: this fork edited neitherplan.mdnor any other doc file — fully compliant, unlike the fork that produced the Core item above. -
LDtk/*(LDtk JSON document model + integration) — COMPLETE (2026-07-13). 21 Document DTOs ported as plain public fields + non-friendedfrom_json(matchingTiled/Document's parser-intermediate style, since these types are equally pure/ behavior-free, unlikeTexturePacker's private+friend style).LDtkTilemapDataConverterconverts one already-selectedLDtkLevel(LDtk projects are multi-level; iterating levels isLDtkJsonParser's job, not the converter's), followingTiled::Converters::Convert's established two-stage shape.LDtkJsonParseralso exposes LDtk-only convenience API present on upstream's own public surface (ParseLevel/ParseAllLevels/ParseWorld/GetWorldIdentifiers/GetTableOfContents/GetWorldPosition/FindLevelByIid) plus a genuinely-LDtk-specificCanParse(filePath)— independently confirmed absent from the sharedITilemapParserinterface and fromTiledTmxParser.cs(zero references), so this isn't a gap in the already-landed interface, just a real per-format difference. Realstd::stoileniency bug found and fixed in this port's own new hex-color-parsing code (same class of bug as theXmlReaderExtensions.cppfix earlier this session — doesn't validate full-string consumption): fixed with aTryParseHexBytehelper checking the parse position reached the string's end. 21 fresh tests (hand-authored JSON fixture; no upstream test uses a portable fixture format). One test-writing quirk self-caught and fixed: an assertion incorrectly expectedParseColor("#000000")to equalColor::Transparent, whenParseColoralways forcesAlpha=255— meaning upstream's owncolor != Color.Transparent || colorString == "#000000"fallback clause is dead code on the!=side; corrected the test's expected value and documented the upstream dead-code quirk inline rather than silently dropping it. -
Ogmo/*(Ogmo Editor JSON document model + integration) — COMPLETE (2026-07-13), ported in parallel withLDtk/*(confirmed genuinely independent: disjoint files, no shared dependency). 16 Document DTOs withfrom_json, matching the same JSON convention.OgmoLayerDataConverter's polymorphic layer-type dispatch is a plain JSON-key-presence check (tileset/data/data2D→ tile;grid/grid2D→ grid;entities→ entity;decals→ decal) — not reflection, translated directly.OgmoTilemapDataConverterfollows the sameTiled-established two-stage architecture, with Ogmo-specific logic for tileset PNG-header dimension reading, 1D/2D/coord tile decoding, and custom-value-to-property mapping. Genuine upstream quirk preserved, not fixed (independently confirmed by reading the exact line):Convert'sNamefield is computed viaPath.GetFileNameWithoutExtension(level.OgmoVersion)— applying a filename-extension strip to a version string (e.g."3.4.0"→"3"), almost certainly an upstream copy-paste bug (probably meant to strip the actual file path), ported exactly as-is. 22 fresh tests, including a synthetic in-memory PNG-header resolver to exercise tileset dimension reading without a real texture (matching this project's standing no-live-GraphicsDevice-test-infra boundary). - Explicitly skip:
Tilemaps/Content/*Reader(xnb-based, see §2 exclusions) - COMPLETE (2026-07-14): ported
tests/MonoGame.Extended.Tests/Tilemaps/Rendering/TilemapIntegrationTests.cs(28 tests, expanded to 56 executed GoogleTest cases viaTEST_P/INSTANTIATE_TEST_SUITE_Pfor the 4 upstream[Theory]methods overTilemapTileFlipFlagscombinations) — the last upstream test file for this phase. Unlike the unit/validation-style tests already ported for all 4 renderer classes ("does it throw"/"does it not throw"), this is a dedicated pixel-verification suite: renders to a headless render target and asserts on actual rendered pixel colors, covering bothTilemapRenderer(GdRenderer_*) andTilemapSpriteBatchRenderer(SbRenderer_*) — tile placement, hidden/zero-opacity layers, tint/partial-opacity, camera position/zoom, animation frame advancement, allTilemapTileFlipFlagscombinations, layer groups/merged mode, parallax, reload cycles, interleaving with plainSpriteBatchdraws. All 28 ported faithfully, none found unportable. Real CNA-vs-XNA API-shape gap found and worked around (not a production bug): a literal translation of upstream'srenderTarget.GetData(pixels)failed every test — this port'sTexture2D::GetDataonly returns a CPU-side mirror populated bySetDatauploads, never touched by GPU rendering into aRenderTarget2D. The correct, already-established idiom incnafor reading back real rasterized GPU content isGraphicsDevice::GetBackBufferData(...)(a realglReadPixels-backed readback of whichever framebuffer is currently bound), called before unbinding the render target — confirmed directly againstcna's ownexamples/easygl_rt_roundtrip_test.cpp("Read RT1 pixel while FBO is still bound"). Once fixed, every pixel assertion (exact-equality white/black/red-tint/quadrant- orientation checks) passed against the real renderer output with no loosened tolerances — confirms no rendering bug inTilemapRenderer/TilemapSpriteBatchRenderer/BasicEffectitself. Independently re-verified by the orchestrating session (not just trusted): cleangit status(only the one new test file), genuinely cleanrm -rf build+build-headersrebuilds of both configs (zero warnings), fullctestre-run (2042/2042 passing, up from 1986), and theGetBackBufferData-before-GetDataclaim confirmed directly againstcna's own example source rather than trusted as reported. Phase 7 is now 100% complete.
Depends on Phases 1 and 5 (rendering) — both complete. Ported in parallel with Phase 7's Tilemaps core (confirmed genuinely independent: no shared files, no dependency relationship).
- Core:
ParticleEffect,ParticleEmitter,ParticleBuffer,ParticleIterator,ParticleRenderingOrder— COMPLETE (2026-07-13).Data::Particle: upstream's[StructLayout(Pack=1)] unsafe structwithfixed float[N]inline arrays ported as a plain packed C++ struct with ordinary inline arrays — nounsafe/pointer tricks needed, this is native C++ territory. **ParticleBuffer**: rawstd::malloc/std::free(deliberately notnew[], which would default-construct every slot, unlike upstream's genuinely-unmanaged allocation),Particle*pointer arithmetic throughout. Independently spot-checked the ring-buffer head/tail wraparound logic (Release/Reclaim) against upstream line-by-line — exact match, including theBufferEnd/Size + 1wraparound arithmetic. - Remaining
Particles/**(profiles, modifiers, primitives) — COMPLETE (2026-07-13), 45 of 47 files (see the one explicit exception below).Modifiers/Interpolators/ InterpolatorOfT.cs(genericInterpolator<T>) renamedInterpolatorOfT— C++ disallows a class template and non-template class sharing a name, matching theComponentMapper<T>→ComponentMapperOf<T>precedent from the just-landed ECS module. A reflection-in-constructor hazard in theModifier/Interpolatorbase classes (Name = GetType().Name) was fixed the same way as this session's establishedGameTimerprecedent: an explicitnameconstructor parameter, each concrete subclass passing its own literal name. Two genuine upstream quirks preserved, not "fixed":ModifierExecutionStrategy::Parallelhas a real, upstream-original data race (every modifier's update runs concurrently against one shared mutable iterator, not synchronized);Rectangle{Loop}ContainerModifierapplies(int)truncation inconsistently across its four boundary checks. -
ParticleEffectSerializer.cs(1226 lines) — COMPLETE (2026-07-13), follow-up pass after the rest of the module landed. No reflection anywhere upstream (pure manual type-name dispatch both directions, as anticipated). The read side is built onSystem::Xml::XmlDocument/XmlElement/SelectSingleNode/SelectNodes(DOM), notXmlReader(sharp-runtime'sXmlReaderlacksReadToDescendant/ReadSubtree) — same patternBitmapFontFileReader.cppalready established. Preserved upstream quirks, not fixed: a<Modifier>must explicitly specifyFrequencyin XML (throws otherwise, since the setter rejects ≤0 and the default read value is 0);ReadInterpolator's unrecognized-Typepath is a null-deref crash in C#, translated to a catchablestd::logic_errorper this session's established convention. Real, pre-existing bug found and fixed (silently broken since Phase 6, would have made everySerializepath in this file fail):Serialization/Xml/ XmlWriterExtensions.cpp'sWriteAttributeFloat/Vector2/Vector3usedstd::to_string(float), which always emits fixed 6-decimal notation ("60.000000"), not C#'s shortest-round-trippable$"{value}"("60") — switched to the already- existingSystem::Single::ToString(std::to_chars-based), matching upstream exactly. Also addedParticleEmitter::ownedTexture_/AdoptOwnedTexture(mirroringTilemap/BitmapFont's established loaded-texture-ownership pattern — a texture loaded specifically for this emitter's region needs an explicit C++ owner, unlike C#'s GC). 57 fresh GoogleTest cases ported from upstream's 2183-line test file (all 8 profiles, all 11 modifiers, all 6 interpolators, both directions). Two upstream null-argument tests not portable (this port'sDeserializetakes references, not nullable pointers).AssertParticleEffect's test helper compares attribute/tag sets rather than exact document strings, since tinyxml2 doesn't match .NET'sXmlWriterbyte-for-byte (case, whitespace, indent) — same precedent already established byXmlWriterExtensionsTests.cpp. - Explicitly skip:
Content/ContentReaders/ParticleEffectContentReader.cs(xnb-based). - Port
tests/MonoGame.Extended.Tests/Particles/**— COMPLETE.RingProfileTests.cs/PointProfileTests.cs/ParticleEffectSerializerTests.csported 1:1 (the only upstream test files that weren't themselves stale/commented-out dead code —AssertionModifier.cs,EmitterTests.cs, andParticleBufferTests.cswere checked directly and found to be substantially or entirely stale against the current upstream API, so those sections were adapted or re-created fresh against the real API instead of ported verbatim). Fresh GoogleTest coverage added for every other type with no upstream test (all 6 interpolators, all 11 modifiers, all 8 profiles,LineSegment,ParticleEffect,ModifierExecutionStrategy, the 4Particle*Parametertypes) — 96 tests total (39 + 57).
Depends on Phase 1 (Bag<T> from Collections is used internally by Artemis-style ECS
implementations — confirmed and reused, not re-rolled). Ported in parallel with Phase 6
(confirmed genuinely independent beforehand: zero references from upstream's ECS/ folder
to System.Text.Json/MonoGame.Extended.Serialization anywhere).
-
World,WorldBuilder,Entity,EntityManager— COMPLETE. Ownership model: upstream pools and reusesEntityobjects viaPool<Entity>, relying on the GC to keep every createdEntityalive for as long as the pool's free list or an active_entityBagslot references it.EntityManagerhere owns everyEntityever created inallEntities_(std::vector<std::unique_ptr<Entity>>) for pointer stability;entityPool_/entityBag_hold non-owningEntity*, recycled the same way upstream's pool reuses them — matching the "canonical ownership lives somewhere for the whole EntityManager's lifetime, only where changes (GC root vs. explicit owning container), not the observable recycling behavior" reasoning, same derive-from-upstream-behavior approach already used forScreenManager/Texture2DRegion.Worldsimilarly owns every registeredISystem;updateSystems_/drawSystems_hold non-owningIUpdateSystem*/IDrawSystem*obtained viadynamic_cast(translating upstream'ssystem is IUpdateSystempattern-match checks).WorldBuilder::Build()returnsstd::unique_ptr<World>, notWorldby value (a C++ value-type return doesn't fit a type that ownsunique_ptrmembers). -
Aspect,AspectBuilder,ComponentBits,ComponentManager,ComponentMapper,BitArrayExtensions— COMPLETE.ComponentType.csexplicitly not ported: confirmed by reading the actual file — it is entirely commented-out dead code upstream (as is its would-be test,ComponentTypeTests.cs), not a real type.ComponentMapper<T>renamedComponentMapperOf<T>(C++ disallows a class template and non-template class sharing a name; C# permits arity-based overloading of type names).IComponentMapperService(a generic-method interface) has no direct translation — C++ disallows virtual member-function templates — soComponentManager(its only implementer anywhere upstream) is used directly instead. Reflection gap, documented not silently resolved: upstreamComponentManager'sthis[Type type]indexer usesActivator.CreateInstance(...MakeGenericType(type)...)to construct a mapper for a type known only at runtime — no C++ equivalent.GetMapper(std::type_index)can only return a mapper already created via the genericGetMapper<T>()path; documented at length inComponentManager.hpp. -
EntitySubscription— COMPLETE. -
Systems/*(ISystem,EntitySystem,UpdateSystem,DrawSystem,EntityUpdateSystem,EntityDrawSystem,EntityProcessingSystem) — COMPLETE.EntityUpdateSystem : EntitySystem, IUpdateSystem(andEntityDrawSystem) reachISystemvia multiple inheritance paths — required makingISystema virtual base ofIUpdateSystem/IDrawSystem/EntitySystemto avoid ambiguous subobjects and brokendynamic_cast, a genuine C++-only concern with no C# analogue (interfaces don't have this diamond problem). - Port
tests/MonoGame.Extended.Tests/ECS/**— COMPLETE. 6 of 7 upstream test files ported (ComponentTypeTests.csskipped — tests the deadComponentType, see above). The portedComponentManagerTests' indexer test adapted for the reflection-gap difference noted above (callsGetMapper<T>()first).
- End-to-end example (
examples/tiled_demo/, ported via a forked sub-agent): loads a real hand-authored Tiled TMX map (assets/map.tmx+ a real external tileset/player-sprite BMP, resolved through the default local-filesystemExternalResourceResolver— not a synthetic in-codeTilemap), renders it viaTilemapSpriteBatchRenderer, drives a 3-frame walk-cycleAnimatedSprite(viaTexture2DAtlas/SpriteSheet/SpriteSheetAnimationBuilder/AnimationController) along scriptedKeyboardStateExtendedinput (plus a separately-wiredInputListeners::KeyboardListenerdemonstrating the event-driven API), and anOrthographicCamerafor the view/projection transform. Runs headlessly using this project's own established test idiom (realGraphicsDevice+ off-screenRenderTarget2D+GraphicsDevice::GetBackBufferDatareadback — seeMISSING.mdfor whyTexture2D::GetDataalone doesn't work here), sampling real rendered pixels each frame and saving the final frame as a PNG. Independently re-run and visually confirmed by the orchestrating session (not just trusted): the saved PNG shows exactly the intended grass field, dirt road, and player sprite. Two real, non-blocking rough edges found and documented (not fixed): (1)GraphicsDevice::GetBackBufferData(Color*, int)(no explicitRectangle*) reads against the window's real backbuffer size, not the currently-boundRenderTarget2D's size — worked around with the explicit-Rectangleoverload; worth a doc note nearerGetBackBufferDataitself. (2)AnimationController::AdvanceFrameonly honorsIsPingPongwhenIsLoopingis also set (faithful upstream behavior, a non-obvious API-combination gotcha, not a port defect). - Full Doxygen pass (via a forked sub-agent, in parallel with the example task):
doxygen Doxyfileinitially failed outright (docs/didn't exist — Doxygen only creates one path level underOUTPUT_DIRECTORY); fixed by creating the already- gitignoreddocs/directory. Found 52 warning lines (45 unique locations) across 20 files: ~37 genuine missing-@paramgaps, 2 files with false-positive#RRGGBB-style hex-code autolink warnings (wrapped in backtick code-spans to suppress), and 4README.md\ref-resolution warnings for cross-references to files outside Doxygen'sINPUTset (converted to plain inline-code mentions). All fixed by hand, matching the established@brief/@param/@returncomment style. Independently re-verified by the orchestrating session, not just trusted:rm -rf docs/generated && doxygen Doxyfile→ exit 0, zero warnings, real HTML output confirmed (4882 files indocs/generated/html). - Zero-warning pass: confirmed clean by the orchestrating session via genuinely
clean
rm -rf build/rm -rf build-headersrebuilds of both configs after all of Phase 10's changes landed (not just the individual forks' own claims) —-Wall -Wextra -Werrorholds in both, full suite 2042/2042 passing. -
README.mdupdated (same Doxygen-pass fork): dropped the stale "~95%/blocked" framing, listed all 9 completed module phases by name with their key types, and added a real usage snippet (Tweener/EasingFunctionsanimating aVector2member) — verified by the fork by actually compiling the snippet standalone against the real headers (caught and fixed one real bug in its own first draft:TweenToreturns a pointer, needs->Easing(...)not.Easing(...)). Also refreshed the "Building" section's CMake option list against the real currentCMakeLists.txt. -
NOTICE.mdre-audited, found already accurate — no changes made. Cross- checked everyplan.mdmention of licensing/attribution (SlimMath/Angle.hpp, nickgravelyn's Triangulator/Triangulation/*.hpp, GAMADU.COM/artemis_CSharp's BSD-licensedBag<T>, the Mercury Particle Engine "Upstream lineage" courtesy section,CircleF/Segment2's academic-only Real-Time-Collision-Detection citation) against the actual current file locations and confirmed each is complete and correctly cited; confirmed the newBasicEffect-substitution work inTilemapRenderer.hpp/TilemapWorldRenderer.hppcarries only the standard MonoGame.Extended MIT header, nothing licensing-relevant was missed. Both forked sub-agents independently re-verified by the orchestrating session before commit per the standing protocol: cleangit status(changes matched exactly what each fork claimed,plan.md/NEXT.md/NOTICE.mduntouched by either), genuinely clean rebuilds of both configs from scratch, fullctestre-run, the example actually re-run and its output/PNG independently inspected, and the Doxygen zero-warning claim independently reproduced.
Phase 10 is complete. All 10 phases of this plan are now complete — the port is done.
- 2026-07-12 — Confirmed MonoGame.Extended license (MIT) permits this port;
cna-extendedwill be MIT withNOTICE.mdattribution. - 2026-07-12 — Reference clone relocated from the user's suggested
/rv/tmpto the established/rv/data/library/github.com/craftworkgames/MonoGame.Extendedconvention (mirrors howcna'sCLAUDE.mdreferences/rv/data/library/github.com/FNA-XNA/FNA). - 2026-07-12 — Full module scope confirmed via user Q&A (see §2). Everything proposed was approved except the already-excluded Content Pipeline / xnb readers.
- 2026-07-12 — Namespace:
CNA::Extended::(notMonoGame::Extended::or flatCnaExtended::). - 2026-07-12 — XNB-based
ContentTypeReaderclasses excluded (dead code without the excluded Content Pipeline). - 2026-07-12 — User approved
plan.md; confirmed strict fidelity (1:1 port as far as C#/C++ differences allow, no simplification) and per-task git commits during the unattended porting session. - 2026-07-12 — Phase 0 CMake target name:
CNA_EXTENDED(matchesCNA/SHARP_RUNTIMEuppercase convention), aliasCNA::Extended(matches the C++ namespace, gives a clean namespaced import target). RootCMakeLists.txtwritten using easy-3d's three-tierTARGET CNA/ opt-in build / headers-only pattern for bothcnaandsharp-runtime. - 2026-07-12 — Standard local build/verify command for this project is
cmake -S . -B build -DCNA_EXTENDED_LINK_CNA=ON(then build +ctest). Discovered while testing the marker interfaces: CNA's XNA types (Vector2,Color,Rectangle, ...) are declared in headers but only defined in CNA's compiled.cppfiles, so a test that constructs one needs CNA actually linked to run, not just compiled.tests/CMakeLists.txtnow follows easy-3d'sCNA_EXTENDED_CNA_LINKED-gated pattern (real gtest executable when linked,OBJECT-library compile-check otherwise) for the whole suite. - 2026-07-12 — Extended
sharp-runtime'sSystem::MulticastAction<Args...>with a token-basedAdd()/Remove()(identity-based unsubscription), approved by the user (this is normally disallowed byCLAUDE.md's "don't modify sibling repos" rule without explicit permission). Needed becauseTransform.cs'sBaseTransform<TMatrix>resubscribes to every ancestor'sTransformBecameDirtyevent on reparenting, unsubscribing the old chain by identity — something C# delegate equality supports natively butstd::functiondoes not.operator+=is unchanged (purely additive change); all 11562 sharp-runtime tests plus 12 newMulticastActiontests pass. Expect this same need to recur elsewhere in the port (any C#event Actionwith a-=) — reach forAdd/Removefirst before inventing another local workaround. - 2026-07-12 — Bounding volumes ported via a forked sub-agent (to keep ~3200 lines of C#
source-reading out of the orchestrating session's context), then reviewed and corrected
by the orchestrating session before commit: the fork's port had silently dropped
GetHashCode()from all 5 types (a real fidelity gap — C#'s equality-override convention pairsEquals/GetHashCode). Fixed by adding it to all 5 types, followingsharp-runtime'sArraySegment<T>::GetHashCode()convention (int GetHashCode() const, XOR-combining member hashes) for 4 of them, andsharp-runtime'sSystem::HashCodeforBoundingPolygon2D(matches upstream'sHashCode.Add/ToHashCode()loop over vertices) — noteSystem::HashCode::Add<T>needsstd::hash<T>, which doesn't exist forVector2, so it's fedvertex.GetHashCode()(anint) rather than theVector2itself. Lesson for future forked porting tasks: explicitly review for droppedEquals/GetHashCode/operator-overload triads, not just "does it build and pass its own tests" — a fork's self-reported build/test success does not by itself guarantee nothing was silently omitted. - 2026-07-12 —
Line2D/LineSegment2D/Ray2Dported via a forked sub-agent, this time explicitly instructed to self-check theEquals/GetHashCode/ToString/operators/Deconstructchecklist per type before reporting back (the lesson from the bounding volumes task) — independently re-verified viagrep -n "public "against the upstream.csfiles regardless, and the self-check held up. The fork also caught and reported a real mistake inBoundingCapsule2D.hpp's existing deferral comment (see the Phase 1 checklist entry above) rather than silently working around it or ignoring it — a good outcome from asking forks to state deferral reasons, not just deferral lists. - 2026-07-13 —
RectangleF/Rectangle.Extensions/RectangleF.Extensions/BoundingRectangleported via a forked sub-agent (OrientedRectanglefully deferred — needsMatrix3x2, see the Phase 1 checklist). Explicitly asked the fork to check whether landingRectangleFactually unblockedIRectangularF(IRectangular.hpp) andCamera<T>'s test — it verified both were genuinely unblocked (not assumed) and landed both follow-ups in the same pass, updatingCamera.hpp's forward-declare to a real#include. Independent re-verification viagrep -n "public "againstRectangleF.csconfirmed theEquals/GetHashCode/ToString/operators checklist held up again. - 2026-07-13 —
CircleF/EllipseF/Segment2ported via a forked sub-agent. Found (and faithfully preserved, per the standing no-simplification mandate) a genuine upstream bug:Segment2.SquaredDistanceTosilently drops areturnfor points beyond the segment'sEnd(see the Phase 1 checklist entry above for the exact line and confirmed wrong value) — independently re-verified by readingSegment2.cs:96directly, confirmed the fork's finding is accurate. This is the first confirmed upstream bug found during porting; if another turns up, apply the same rule: preserve it, document it prominently in the header, cover it with a regression test that names the discrepancy explicitly — never silently "fix" it while porting. - 2026-07-13 —
Size/SizeF/Interval/Thicknessported via a forked sub-agent, with an explicit heads-up in the prompt about the three specificSizeF-blocked follow-up spots (ISizable,RectangleF,BoundingRectangle) to check onceSizeFlanded. All three verified genuinely unblocked and landed in the same pass — see the Phase 1 checklist entry above for exactly what was added to each. User also asked mid-task to push work so far to GitHub, and to create + push a newdevelopbranch; both done (masteranddevelopboth exist onoriginas of this session; ongoing work commits todevelopgoing forward per the user's instruction). - 2026-07-13 —
Matrix3x2/MatrixExtensions/Vector2Extensionsported via a forked sub-agent, with an explicit heads-up aboutMatrix3x2being the single most-referenced blocker so far. LandedTransform2as a verified bonus follow-up in the same pass (see Phase 1 checklist). Precisely distinguished "unblocked" from "partially unblocked" for the other flagged spots —RectangleF/BoundingRectangle'sTransformandCircleF/Segment2's remaining deferrals are still blocked onPrimitivesHelperspecifically, not Matrix3x2;OrientedRectangleis now partially unblocked (constructor + field only needSizeF+Matrix3x2, both available) but itsTransformmethod still needsPrimitivesHelper— flagged as a good near-term small follow-up task rather than guessed at. This session's second confirmed bug, this time in cna-extended's own new code (not upstream): a static-initialization-order fiasco inMatrix3x2::Identity(built from another translation unit's staticVector2constants), caught by 2 failing tests and fixed with literal values. Worth remembering as a general C++ hazard for any futurestatic const Type X = Type(other-class's-statics...)pattern, not just this one instance. - 2026-07-13 — Picked up the
OrientedRectanglefollow-up immediately (ported directly, no fork — small enough by this point). Re-scoping it turned up a correction to the previous task's fork report:BoundingRectangle(the property) andexplicit operator RectangleF(OrientedRectangle)are also transitively blocked onPrimitivesHelper(both callRectangleF::Transforminternally), not just the staticTransformmethod as previously reported. A reminder that "only X is blocked" claims from a fork (or from this session's own prior notes) are worth re-verifying by reading the actual dependency chain, not just trusting the most recent summary — the same discipline already applied to fork self-reports applies to this session's own carried- forward notes too. - 2026-07-13 —
FastRandom/RandomExtensionsported directly (small enough, no fork). Deliberately did NOT reusesharp-runtime's ownSystem::Random::NextSingle()forRandomExtensions::NextSingle, even though it exists and "reuse sharp-runtime, don't re-roll" is the usual rule — that method implements a different (more modern, matching real .NET 6+) algorithm than whatRandomExtensions.csitself actually does (a plain(float)NextDouble()cast). The "reuse, don't re-roll" principle is about not duplicating equivalent functionality; it does not override the fidelity mandate when the existing utility's behavior actually differs from what upstream's specific file does. Worth keeping in mind for any future case where a sharp-runtime type has a same-named member that isn't a drop-in behavioral match for what's being ported. - 2026-07-13 —
PrimitivesHelperported directly (no fork; fully self-contained, ~145 lines). While scopingShapeExtensions(deferred whole to Phase 5 — pureSpriteBatchdebug-drawing code, not a math utility despite itsMath/folder location), discovered its dependency onMonoGame.Extended.Shapes.Polygon/Polyline, an entire upstream folder never tracked anywhere in this plan — a real gap, not an intentional exclusion. Both types were small and fully self-contained, so ported them alongside this task rather than leaving the gap open; first use of aCNA::Extended::Shapessub-namespace. Then, rather than assumingPrimitivesHelperunblocked the severalRectangleF/BoundingRectangle/OrientedRectangledeferrals flagged by name in earlier sessions' notes, independently re-verified each by reading the current header's deferral comment plus the actual upstream.csimplementation before touching any code (the same discipline established in the 2026-07-13OrientedRectangleentry above, now applied to a 3-type sweep instead of a single correction). ConfirmedLine2D/LineSegment2D's deferrals are genuinely still blocked onCollision2D, notPrimitivesHelper, despite citing an overlapping algorithm source — left those deferred rather than guessing they were unblocked because a plausibly-related type had just landed. Landed the genuinely- unblocked follow-ups in the same commit:RectangleF::Transform/CreateFrom(points)/UpdateFromPoints/SquaredDistanceTo/DistanceTo/ClosestPointTo;BoundingRectangle's equivalents;OrientedRectangle::Transform/getBoundingRectangleProperty()/operator RectangleF(). PortingOrientedRectangle's upstreamTransformtest class surfaced a test-infrastructure fidelity question, not a code one: 2 of the 9 tests use upstream'sCollectionAssert.Equalhelper, which is order-insensitive (contains-check, not sequence-equality) — a literalEXPECT_EQport of those two tests' expected point lists failed the build even though the underlyingTransform/getPointsProperty()math was correct, because upstream's own hand-written expected order doesn't match its ownPointsgetter's algorithmic order either (it only passes upstream because the helper ignores order). Resolved by writing a smallExpectUnorderedPointsEqualhelper that replicates the C# helper's actual semantics for just those 2 tests, rather than silently reordering the expected values to makeEXPECT_EQpass — porting a test faithfully means porting what it actually asserts, not just its literal C# structure. Widened the sweep beyond just the 3 flagged types: grepped the whole tree for "PrimitivesHelper" in deferral comments and found 2 more genuinely- unblocked spots not previously called out by name —CircleF's 4-overload (collapsed to 2)Intersects(CircleF, BoundingRectangle), which callsBoundingRectangle::SquaredDistanceTo, andSegment2'sIntersects(RectangleF|BoundingRectangle, out Vector2), which callsPrimitivesHelper.IntersectsSlabdirectly — landed both in the same commit with fresh tests (upstream's own coverage for all of these is entirely commented out in its test suite, so nothing to port 1:1). Test suite grew from 429 to 464 (35 net new tests); both-DCNA_EXTENDED_LINK_CNA=ONand headers-only builds verified clean. - 2026-07-13 —
Math/Triangulation(6 upstream files) ported via a forked sub-agent. Before delegating, discovered all 6 files citeMIT Licensed: https://github.com/nickgravelyn/Triangulatorin their own headers — the same "code directly derived from a different MIT project" situation asAngle.cs/SlimMath, not the usual "algorithm citation in a comment" pattern seen elsewhere inMath/. Did the license diligence myself (not delegated): fetched the cited URL directly (404) and via the GitHub API (404), searched for mirrors/archives/renamed accounts (none found), confirmed via web search that multiple independent sources agree the project was MIT licensed but none could supply the exact original copyright wording. Added aNOTICE.mdentry using the standard MIT template with an explicit provenance caveat rather than guess at wording, and gave the fork the exact SPDX header text to place in all 6 files so it wouldn't need to make that judgment call itself. Fork'sCyclicalList<T>/IndexableCyclicalLinkedList<T>design choice (composition aroundsharp-runtime'sList<T>/LinkedList<T>rather than replicating C#'snew-keyword-hiding subclassing) is a good precedent for any future upstream type that inherits from a C# collection purely for internal use. Independently re-verified the fork's work before committing (per this session's standing "never just trust a fork's self-report" rule): re-grepped all 6 upstream files forpublic |internalmembers against the ported headers (nothing dropped), hand-tracedCutHoleInShape— the trickiest method, with nullable-comparison logic and cyclical-list index injection — line-by-line against the C# source (faithful), and did a cleanrm -rf build+ full reconfigure/rebuild myself rather than trusting the fork's already-builtbuild/directory, confirming zero new compiler warnings and 503/503ctestfrom scratch. No upstream bugs found this time — the algorithm translated cleanly. - 2026-07-13 —
FramesPerSecondCounter/FramesPerSecondCounterComponentported directly (no fork; tiny, ~100 lines of C# total). Firstcna-extendedtype to implement CNA's ownIUpdateableinterface and derive fromSystem::Object— followed CNA's ownGameComponent.hpp/.cpppattern closely rather than inventing a new one (publicEventHandler<EventArgs>members,getXChangedEvent()accessor overrides,Raise(this, EventArgs::Empty)for firing). Found this session's third confirmed upstream bug:FramesPerSecondCounter.UpdateOrder's setter raisesEnabledChangedinstead ofUpdateOrderChanged(a copy-paste error from theEnabledsetter directly above it) — preserved exactly, documented prominently, covered by an explicit regression test naming the discrepancy, matching theSegment2.SquaredDistanceToprecedent rather than "fixing" it.FramesPerSecondCounterComponentneeds a liveGame&to construct (it's aDrawableGameComponent); confirmed viacna's own test suite (DrawableGameComponentTests.cpp) that this project's sibling repo already established the precedent of not unit-testing that base class for exactly this reason — followed the same precedent rather than inventing test infrastructure CNA itself doesn't have. Caught and fixed one of my own test-authoring mistakes before landing: an initial test assumedFramesPerSecondCounter's internal timer starts at zero, but it actually starts pre-loaded at a full second (matches upstream's own_timer = _oneSecondTimeSpan;field initializer) — meaning the very firstUpdate()call always latches aFramesPerSecondreading immediately, regardless of how little time has actually elapsed. Traced the arithmetic by hand to confirm this is correct upstream behavior (not a fidelity bug in the port), then rewrote the test to assert the real behavior instead of masking it. - 2026-07-13 —
SimpleGameComponent/SimpleDrawableGameComponentported directly (no fork; ~127 lines of C# total). First time this port had to translate C#'s explicit interface implementation — decided the policy case-by-case rather than applying one blanket rule: where upstream itself used two different names for the public property vs. the interface-only one (IsEnabledvs.IUpdateable.Enabled), preserved both as distinct C++ members (public getter + a private override of the interface method, reachable only through a base-interface reference — the closest C++ analog to "interface-only access," and legal C++: access specifiers gate name lookup, not virtual dispatch, so a private override is still called correctly through a base pointer/reference). Where upstream reused the identical name for both (Visible/IDrawable.Visible), there was no distinct name left to preserve, so collapsed to one public override — documented as a naming/visibility simplification only (both C# members always read the same backing field regardless, so no behavior differs either way). Worth remembering as the general rule for any future explicit-interface- implementation case: check whether upstream gave the two members different names before deciding whether a two-member or one-member C++ translation is the more faithful choice. - 2026-07-13 —
Bag<T>/Deque<T>ported via a forked sub-agent (combined ~1053 lines of C# source + 456 lines of upstream tests, well past the fork threshold). Before delegating, discoveredBag.cscarries a BSD-2-clause-style license from a different upstream project (GAMADU.COM/artemis_CSharp), not MonoGame.Extended's usual MIT header — handled that license research myself (see the Phase 1 checklist entry above for the full account) and gave the fork the exact SPDX header text to use, the same pattern established for theTriangulation/nickgravelyn case. The fork's report claimed to have found three genuine upstream correctness bugs inDeque<T>(not just fidelity/GC-hygiene quirks like everything found before this) — including two ("RemoveAt's back-half-shift is broken even unwrapped" and "front-half- shift is ALSO broken once wrapped") it said were discovered empirically, by writing a test that asserted the mathematically-correct result, watching it fail against a faithful 1:1 translation, then hand-tracing to confirm the failure was inherent to upstream's algorithm rather than a translation defect. Given the significance of that claim — "upstream's Deque has real data-corruption bugs" is a much bigger claim than "this rounds slightly differently" — did NOT just trust the fork's self-report or its test's assertions at face value. Independently re-derived all three bugs from scratch: hand-tracedIndexOf's formula andRemoveAt's middle-index branches against the actual upstream.cssource using fresh concrete examples I constructed myself (not the fork's examples), computing expected physical-array and logical-view state by hand at each step. All three bugs reproduced exactly as claimed, and the fork's specific test-expected-values (e.g. the exact post-removal element sequence inRemoveAtMiddleShiftsShorterBackHalfReproducesKnownUpstreamBug) matched my independently-computed values precisely. This is the deepest independent re-verification a fork's bug-finding claim has received in this project so far, given the stakes (silent data corruption is a much more serious class of finding than a rounding/formatting quirk) — worth treating "this fork found a real bug, not just a fidelity note" claims as needing proportionally more scrutiny before committing, not just the standard self-check-plus-spot-read pass. Also re-ran the full build from a genuinely cleanrm -rf build(not reusing the fork's already-built directory) for both CMake configurations before trusting the reported 589/589 pass count. - 2026-07-13 —
ObjectPool<T>/Pool<T>/IPoolable/ItemEventArgsported directly (no fork; small, 244 lines of C# total). Found this session's fourth confirmed upstream bug, and by far the most severe: a genuine infinite loop inObjectPool<T>. GetEnumerator()for the single most basic usage pattern (create one pooled object, enumerate the pool) — see the Phase 1 checklist entry above for the full mechanism. Unlike theDeque<T>bugs (found by a fork, then independently re-verified by me), this one was found by me directly while hand-tracingCreateObject()/Use()during the port itself — a useful reminder that reading upstream source line-by-line during direct (non-forked) porting surfaces real bugs just as often as adversarial re-verification of a fork's claims does; the "verify carefully" discipline this session established isn't only for reviewing other agents' work. Given the bug is a literal infinite loop, took explicit care that the regression tests demonstrating it can never accidentally hang the test suite: they assert ongetNextNodeProperty()/getPreviousNodeProperty()directly rather than ever iterating the pool in the exact state that triggers the bug (a prominent warning to this effect is in bothObjectPool.hpp's header andObjectPoolTests.cpp's header, so a future session doesn't "simplify" a test into iteration and reintroduce a hang) — and re-ran the entire suite undertimeout 60as an explicit extra safety net before trusting a clean pass, on top of the normal build+test verification. Also caught and fixed my own test-authoring mistake: initially assumedObjectPool<T>'s full-pool policy applies as soon asTotalCount > Capacityat the very nextNew()call, but upstream's actual guard isTotalCount <= Capacity(allowingCapacity + 1total creations before the policy ever triggers) — traced and corrected before committing, the same "verify, don't assume, even for my own test code" discipline applied throughout this session. - 2026-07-13 —
KeyedCollection/ListExtensionsported directly (no fork;Dictionary Extensionsdeliberately NOT ported — see the Phase 1 checklist entry above). This is the mirror image of theRandomExtensions::NextSinglesituation from task 21: there, a same-namedsharp-runtimemethod turned out to implement a different algorithm and had to be re-ported to preserve fidelity; here,sharp-runtime's ownDictionary<TKey, TValue>::GetValueOrDefaultwas checked line-by-line against upstream'sDictionaryExtensions.GetValueOrDefaultand found to be a true semantic match (both: value-if-present, else a supplied-or-default fallback) — so "reuse, don't re-roll" applies cleanly this time, and no port was needed. Worth remembering as the general rule alongside the earlier lesson: a same-namedsharp-runtimemethod isn't automatically reusable OR automatically to-be-avoided — check the actual algorithm each time, and the answer can go either way. - 2026-07-13 — Phase 1's final task, the test-suite parity pass, started with a direct
personal audit (not delegated) of all 16 upstream files under the plan's literal
4-folder wording (
Math/Primitives/Shapes/Collections) — confirmed 100% already covered. Then deliberately widened scope past that literal wording to root-level upstream test files testing Phase-1 types, since the task's actual purpose (full Phase-1 parity) is broader than 4 named subfolders — a plaingrep -c '\[Fact\]\| \[Theory\]'count comparison between each upstream file and its ported counterpart surfaced a real, substantial gap in 6 bounding-volume/line-segment files (up to 20 missing tests in one file). This is a useful precedent: when a task's literal written scope and its evident purpose diverge, checking the purpose-implied scope (here, cheap: a fewgrep -ccalls) is worth doing before declaring a task done on the literal wording alone. Delegated the confirmed gap-filling to a forked sub-agent (7 files to check for real gaps, 8 more for a lighter verification pass) with explicit instructions to distinguish "genuinely un-ported, portable now" tests from "correctly deferred to Phase 2 pendingCollision2D" tests by reading each type's own header-comment- documented deferral list, not by guessing from test names, and to flag (not silently paper over) any case where a test needed functionality a header comment claimed was already ported but which turned out not to actually exist. None were found — every header's documented deferred-vs-ported split held up exactly. Independently re-verified 2 of the fork's 6 modified files by reading the actual diffs against the upstream.cssource line-by-line (including the most calculation-heavy new tests, e.g.OrientedBoundingBox2D::CreateFromRotation/Transformwith non-uniform scale) — all matched exactly — then did a genuinely cleanrm -rf build+ full rebuild of both CMake configurations myself before trusting the reported 663/663 pass count, the same "never just trust a fork's self-report" discipline applied to every fork in this session. Phase 1 is now complete: all 30 tasks landed, fromVersionthrough this test-parity pass, spanning ~50 forked-and-direct porting tasks, ~130 ported/created source files, and a test suite that grew from 0 to 663 passing tests. Four genuine upstream bugs were found and faithfully preserved along the way (Segment2. SquaredDistanceTo's missingreturn,RectangleF.Extensions.Clip's mutate-before- derive ordering,FramesPerSecondCounter.UpdateOrder's wrong-event-raised copy-paste error, andObjectPool<T>'s severe self-referencing-node infinite-loop bug), plus two confirmed-and-preservedDeque<T>correctness bugs (IndexOf's not-found handling andRemoveAt's middle-index shift logic) found by a fork and independently re-verified. Next perplan.md§5: Phase 2 ("Collisions 2D"), which unblocks the large cluster ofIntersects/Contains/TryGetCollisionmethods deferred throughout Phase 1 — seeNEXT.mdfor the concrete next-task pointer. - 2026-07-14 — Full member-level API audit, spanning the entire porting plan (all
~390 upstream files, every module), requested by the owner as a deeper follow-up to
the earlier file-existence-only completeness audit (see Phase 6's entry above).
10 parallel forked sub-agents each covered a coherent module group, comparing every
public member (method/property/field/event/operator/constructor) of every ported type
against its upstream
.csfile, not just checking that a corresponding file/type existed. Two forks initially returned truncated "still waiting on my own sub-forks" status updates instead of real reports (the same failure mode documented earlier this session for top-level forks, this time occurring recursively inside forks that tried to further sub-delegate) — both resolved by an explicit "stop delegating, finish synchronously yourself" instruction, matching this session's standing "verify, don't just trust, and be prepared to intervene" fork discipline. Result: 4 of 10 module groups (166 files) had zero issues; 6 real, genuine gaps found across the other 6 groups, all independently confirmed against source (not taken on a fork's word) and then fixed:ColorHelper::FromHexonly had aconst std::string&overload; upstream also has a non-allocatingFromHex(ReadOnlySpan<char>). Consolidated to a singleFromHex(std::string_view)overload (aconst std::string&sibling overload was tried first and reverted — it made every string-literal call site ambiguous, since a literal converts to bothstd::stringandstd::string_viewwith equal rank;std::string_viewalone already acceptsstd::stringimplicitly, unlike C#, so one overload covers both upstream signatures).Matrix3x2::ToMatrix(float depth, Matrix& result)(instance and static) had nodepth-defaulted zero-arg counterpart, unlike the by-value-return overloads one line above withdepth = 0.0f. Fixed by adding genuinely separateToMatrix(Matrix& result)/static ToMatrix(const Matrix3x2&, Matrix& result)overloads (a default argument isn't possible here — C++ requires trailing parameters to be the ones defaulted, andresultis the last, non-defaultable parameter).Tweener::ActiveTweens(upstreamReadOnlySpan<Tween>, iterable) had been narrowed togetActiveTweensCountProperty()(count-only) with no documented rationale, unlike every other deliberate narrowing in that file. AddedgetActiveTweensProperty()returning a materializedstd::vector<Tween*>(documented as not zero-copy like upstream's span, since this port'sstd::vector<std::unique_ptr<Tween>>storage has no direct zero-copyTween*view — but the same practical capability).- A real correctness bug, not just a missing member:
Serialization/Xml/XmlNodeExtensions.cpp'sParseInvariant<T>()used uncheckedstd::istringstreamextraction, silently returning a default/truncated value for malformed numeric/boolean input instead of throwing — contradicting both upstream'sConvert.ToXxxsemantics and this exact file's own header comment, which already (correctly) documented the intended throw-on-malformed-input contract. The same class of bug already found and fixed once this session in the siblingXmlReaderExtensions.cpp, but this second occurrence inXmlNodeExtensions.cpphad gone uncaught — no test exercised the malformed-input path. Fixed using the sameSystem::Byte::Parse/System::UInt16::Parse/System::Int16::Parse/System::UInt32::Parse/System::Int32::Parse/System::Single::Parse/System::Double::Parse/System::Boolean::Parse/System::SByte::Parsepattern already established inXmlReaderExtensions.cpp; also fixedGetBoolAttribute(previously accepted"1", upstream'sConvert.ToBooleandoes not) and the two delimited-attribute readers (previously silently left trailing elements at 0 for too few tokens instead of throwing, matching upstream'ssplit[i]out-of-boundsIndexOutOfRangeException). 8 new regression tests added — this file had none for the malformed-input path before. ComponentManager::GetMapper(int componentTypeId)(a trivial direct bag-index lookup, no reflection involved) had no ported counterpart — unlike the file's other two documented, reflection-caused omissions, this one had no representational obstacle and was simply missed. Added directly alongside the existingGetMapper(std::type_index)overload.ParticleEffect::FromFile/FromStream(both trivial delegations toParticleEffectSerializer::Deserialize) were deferred during Phase 8 behindParticleEffectSerializer.csbeing out of scope — but that file was fully ported later in the same phase, and the follow-up to add these two wrapper methods was never done (a stale deferral comment, not a deliberate exclusion). Added, declared inParticleEffect.hppbut implemented inParticleEffect.cpp(includingParticleEffectSerializer.hppback in the header would be circular, since that file already includesParticleEffect.hpp). All 6 fixes independently verified by the orchestrating session: genuinely cleanrm -rf build/rm -rf build-headersrebuilds of both configs (zero warnings), fullctest(2079/2079 passing, up from 2063 — 16 new regression tests), and each new test run individually to confirm it actually exercises the fix (not just compiles). SeeNEXT.mdfor the condensed version of this entry.
Graphics/Effectsshader resources: whether CNA's graphics backend(s) (SDL_RENDERER/EASYGL/BGFX/VULKAN) can consume a portedDefaultEffectas-is or need a backend-specific re-authoring — investigate at the start of Phase 5 and raise with the user if it's a real design fork, not a mechanical port.- Whether
Serialization/*should wrapsharp-runtime'sSystem::Text::Jsondirectly or needs its own converter layer — decide at the start of Phase 6.