Skip to content

Latest commit

 

History

History
1797 lines (1548 loc) · 117 KB

File metadata and controls

1797 lines (1548 loc) · 117 KB

Deferred Implementation Items

Historical gap ledger (2026-08-22): plan.md is now authoritative. Every cited blocker must be re-tested against ../cnanext and ../sharp-runtimenext; resolved items and old workaround recommendations are evidence, not permission to keep a sample-side bypass. Large confirmed gaps go to the owner decision queue in plan.md.

This file tracks features, API gaps, and asset-pipeline issues that block sample ports from compiling or running correctly. Items are roughly ordered by how many samples they affect.

Each item records:

  • What is missing — the specific gap in CNA or sharp-runtime
  • Where to implement — which repository and rough file/module
  • Blocked samples — which sample ports are waiting on this
  • Effort estimate — rough size: S / M / L / XL

1. XNB Asset Pipeline

What is missing: XNA 4.0 samples ship with compiled .xnb binary assets (textures, fonts, models, audio). CNA does not and will not support the XNB format.

What needs to happen instead: All .xnb assets in each sample must be converted to open formats before a CNA sample can load them:

XNB asset type Replacement format
Texture2D PNG (or any format SDL_image supports)
SpriteFont CNA SpriteFont from TTF / bitmap strip
Model glTF 2.0 or OBJ
SoundEffect OGG Vorbis or WAV
Song (music) OGG Vorbis or MP3
Effect (shader) GLSL / HLSL depending on backend

Where to implement:

  • Asset extraction: use MonoGame.Content.Builder or mgcb CLI to convert existing XNB files to source assets.
  • CNA ContentManager must then load each open format directly.

Blocked samples: ALL samples that use Content.Load()

Effort: M per sample (asset conversion) + L for ContentManager loader additions


2. SpriteFont Loading ✅ RESOLVED

What was missing: Content.Load<SpriteFont>("hudfont") — CNA now fully supports this via .font.json + atlas PNG format (SpriteFontTypeReader in ContentManager.cpp).

How to generate a font asset:

python3 tools/make_font.py <path/to/font.ttf> <size_px> <Content/FontName>

This produces Content/FontName.font.json + Content/FontName.png.

Already used in: SafeArea (corner labels, A-toggle hint), InputSequence (move names, player labels, drop-shadow DrawString).


3. EasyGL: DiffuseColor ignored for VertexPositionColor meshes ✅ RESOLVED

Was: EasyGLGraphicsBackend::SelectProgram selects the GLSL shader program purely by vertex stride. VertexPositionColor (stride 16) → EnsureColored3DProgram(), whose shader was FragColor = vColor with no uDiffuseColor uniform, so loc_diffuse = -1 and BindDrawParams never uploaded BasicEffect.DiffuseColor. The mesh always rendered white.

Fix: EnsureColored3DProgram() now declares uniform vec4 uDiffuseColor and the fragment shader outputs FragColor = vColor * uDiffuseColor; prog_colored_.loc_diffuse is wired to it. GpuDrawParams.diffuseColor defaults to {1,1,1,1}, so BasicEffect Ex draws that set no diffuse are unaffected. The non-Ex user-primitive paths (DrawColoredPrimitives / DrawIndexedColoredPrimitives) explicitly upload white, since they carry no diffuse and the uniform would otherwise default to 0 (black). (cna/src/CNA/Internal/Backends/EasyGL/EasyGLGraphicsBackend.cpp.)

Verified: Primitives3D B-key cycles the tint; the cube renders red (was white).

Effort: S


4. EasyGL: Wireframe rendering not possible on OpenGL ES ✅ RESOLVED

Was: FillMode::WireFrame in RasterizerState was silently ignored — OpenGL ES 3.x has no glPolygonMode(GL_FRONT_AND_BACK, GL_LINE).

Fix: ApplyRasterizerState records a wireframe_ flag when FillMode::WireFrame is set. The 3D draw paths then call the new DrawWireframe helper, which re-expands each triangle into three GL_LINES edges ((a,b),(b,c),(c,a)) through a scratch 32-bit line index buffer and draws GL_LINES instead of GL_TRIANGLES. Covers both indexed and non-indexed triangle list / strip draws (honouring startIndex / baseVertex / vertexStart). (cna/.../EasyGL/EasyGLGraphicsBackend.cpp.)

Verified: Primitives3D Y-key shows the cube as an edge-only wireframe.

Effort: M


5. VertexPositionNormal (3D lit rendering) — ✅ RESOLVED for Model-based samples (2026-07-06)

Corrected 2026-07-06: this item claimed CNA had no lit-shader path for per-vertex-normal 3D rendering at all. A live check found that's no longer true: VertexPositionNormalTexture (position+normal+texcoord — the vertex format ContentManager.cpp's ModelTypeReader already produces for any .model.json with normal data, confirmed by reading the reader's SetData dispatch) has a real, tested and passing directional-lighting path in the EasyGL backend: built and ran cna_test_easygl_basiceffect_combinations live — case "(e) Directional lighting — VertexPositionNormalTexture, white tex, red light → reddish pixel" passes (exit code 0). The Vulkan backend also has a dedicated lit_textured3d.{vert,frag}.glsl shader pair. This unblocks all 9 samples previously listed here, since every one of them renders via Content.Load<Model> (confirmed by grep of each one's own C# original) → VertexPositionNormalTexture → this now-working lit path, not a bare, texture-less vertex struct: LensFlareSample (#041), Graphics3DSample (#046, Spaceship.cs loads content.Load<Model>("Models/spaceship") — note lowercase content local var masked this from a naive Content.Load grep), PickingSample (#047 — its own dead-code GeometricPrimitive.cs/bare VertexPositionNormal struct is not compiled into the .csproj; the real runtime path is Content.Load<Model>), TrianglePickingSample (#048), HeightmapCollisionSample (#049), CustomModelClassSample (#052, loads via Content.Load<CustomModel> — a sample-defined custom content type; port can just use the standard tools/obj2model.py conversion + stock Model like every other sample, since CNA has no generic custom-ContentTypeReader extensibility anyway — see item 18), InverseKinematics (#057), ChaseCamera (#058), and MarbleMaze's (#061) EX2/End build. Update each sample's missing.md/plan.md status and re-attempt porting.

Still open — Primitives3D specifically: Primitives3D's original C# ships its own, sample-authored, texture-less VertexPositionNormal struct (Primitives3D/VertexPositionNormal.cs, its own IVertexType — not a built-in XNA type) for its procedurally-generated primitives (sphere/cylinder/etc. have no UV data). CNA has no texture-less normal-lit vertex format, so this specific sample still can't get real per-vertex lighting without either (a) a small CNA addition (a VertexPositionNormal variant + wiring it into the same lit shader), or (b) a port-side workaround (assign a dummy/unused texcoord to every vertex and use the already-working VertexPositionNormalTexture instead) — the port-side workaround is likely the pragmatic choice given VertexPositionNormalTexture is proven and no second sample needs a texture-less variant.

Effort: — (done in cna for the Model-based case) / S (Primitives3D-specific workaround, port-side, no CNA change needed)


14. TextureCube content loading (Content.Load<TextureCube>) ✅ RESOLVED (2026-07-11 confirmed)

Resolved in cna's develop (Task 934, landed 2026-07-10): a real TextureCubeTypeReader was added to ContentManager.cpp's built-in type readers, exactly the shape this item originally asked for. Content.Load<TextureCube>(...) now works directly — RimLighting's/ReachGraphicsDemo's own bypasses (extract DDS faces via ImageMagick, load as Texture2D, copy into TextureCube via SetData()) are no longer strictly necessary, though neither was revisited to use the new reader as part of this documentation update (not blocking, optional simplification only).

What is missing: ContentManager.cpp has no TextureCubeTypeReader registered — Content.Load<TextureCube>(...) throws, even though the underlying pieces it needs already exist: CNA's TextureCube class and DDS decoding are both implemented and proven working (see the easygl_texturecube_* example/tests), and EnvironmentMapEffect is also already implemented.

Where to implement: Add a TextureCubeTypeReader : ContentTypeReader<Graphics::TextureCube> to cna/src/Microsoft/Xna/Framework/Content/ContentManager.cpp's built-in type readers (same shape as the existing Texture2DTypeReader), reading a .dds cubemap file.

Blocked samples: RimLighting (#037) — confirmed via direct source audit (2026-07-05) to have zero custom .fx files; it uses stock EnvironmentMapEffect

  • a TextureCube (OutputCube.dds) purely via Content.Load<TextureCube>. This is the only real gap — unlike the rest of Phase 3, RimLighting does not need the XL HLSL→GLSL shader pipeline (item 11) at all.

Update (2026-07-10, ReachGraphicsDemo #005's EnvmapDemo ported): this sample also needs a TextureCube (its own cubemap, reimplemented at the asset level via a one-off Python script standing in for the original's own build-time CubemapProcessor.cs — see samples/ReachGraphicsDemo/missing.md), but did not need this item fixed: the 6 converted PNG cubemap faces are loaded via the already-proven Content.Load<Texture2D> path, then their pixel data is copied directly into a real TextureCube via its own SetData(CubeMapFace, const Color*, int) API, bypassing ContentManager/TextureCubeTypeReader entirely (the same bypass philosophy as item #26's RawMesh.hpp, just applied to a different CNA type). Confirmed live: the resulting cubemap renders as a genuinely reflective/chrome surface. This item remains open and RimLighting remains its only blocking dependent — ReachGraphicsDemo simply didn't need Content.Load<TextureCube> itself to ship a real, working cubemap.

Update (2026-07-10, RimLighting #037 ported): this item's only blocking dependent is now also unblocked, via the identical bypass. OutputCube.dds is a real, already-baked 6-face DDS cubemap (unlike EnvmapDemo's procedurally-built one), but it's uncompressed (xRGB8888), so TextureCube::DDSFromStreamEXT (CNA's own NOXNA direct-DDS-stream loader, checked first) couldn't be used either — it only decodes DXT1/DXT3/DXT5-compressed cube maps. Worked around identically to EnvmapDemo: extracted each of the 6 faces to an ordinary PNG with ImageMagick (convert OutputCube.dds[N] ... envmap_face.png), loaded via Content.Load<Texture2D>, copied into a real TextureCube via SetData(). Face order confirmed correct (DDS index N ↔ CubeMapFace(N), matching DDSFromStreamEXT's own static_cast<CubeMapFace>(face) loop) — see samples/RimLighting/missing.md for the full account, including live visual confirmation (a clean, correctly-shaped rim-light highlight, not a scrambled or mis-mapped one). This item is no longer known to block any sample in this repo's current task list, though it remains open (cna itself unchanged, per this session's own "do not edit cna/sharp-runtime" constraint).

Effort: S


15. Accelerometer/sensor platform reality — NOT a hard blocker where a fallback fits

What is actually true (confirmed by reading cna/src/Microsoft/Devices/Sensors/Accelerometer.cpp directly, 2026-07-05): Microsoft::Devices::Sensors::Accelerometer is a real, working, SDL3 SDL_Sensor-backed implementation — not a stub. getIsSupportedProperty() gates on CNA::getCurrentPlatform() being Android, iOS, or Desktop (not Android-only), then does a real probe (SDL_InitSubSystem/SDL_GetSensors/ SDL_OpenSensor) for actual sensor hardware. On this project's development machine (desktop Linux, no physical accelerometer chip), that probe correctly returns false — that is a hardware-absence result, not a platform restriction, and the same code path would return true on a real Android/iOS device or a desktop/laptop with a physical accelerometer (e.g. a 2-in-1 tablet). Gyroscope has the identical Android/iOS/Desktop-gated, real-probe shape (see Gyroscope.cpp).

Established working pattern (proven three times — Yacht, SnowShovel, Bounce): when getIsSupportedProperty() is false (this desktop, the normal case), the sample falls back to keyboard/gamepad/touch input that the original XNA sample's own Windows desktop build already shipped (Yacht/SnowShovel/Bounce are all Windows Phone ports that had a #if WINDOWS_PHONE accelerometer branch and a working non-phone input branch in the same shipped C#) — no invented control scheme, just wiring the already-existing fallback path to run unconditionally instead of behind a compile-time #if. See samples/Yacht/missing.md, samples/SnowShovel/missing.md.

Where this pattern does NOT directly apply — audited per-sample (2026-07-05):

  • AccelerometerSample (#084) and TiltPerspective (#107): unlike Yacht/ SnowShovel/Bounce, these two samples' original C# ships with no alternate input path at all — the entire point of each sample is moving a sprite / shifting a 3D perspective purely by tilting the phone (confirmed: no #if WINDOWS_PHONE split, no keyboard/gamepad branch anywhere in Game.cs/AccelerometerHelper.cs). Porting either would mean inventing a keyboard-tilt-emulation fallback that doesn't exist in the original, which is a bigger deviation than Yacht/SnowShovel/Bounce needed (they only had to un-#if an existing branch) — but this project already has precedent for adding input schemes the original never had at all (DynamicMenu/ UISample's touch-fallback patterns, NEXT.md §6). Not a hard blocker — just a bigger design decision than usual, worth confirming with the user before doing it, since "invent the missing half of the sample" is a different scope commitment than "port what's there."
    • Correction (2026-07-10, AccelerometerSample only — found while actually porting it, per user go/no-go): this 2026-07-05 audit's "no alternate input path at all" claim for AccelerometerSample specifically was wrong — a direct read of Accelerometer.cs:117-135 found the original's own GetState() does have a keyboard fallback, gated on Microsoft.Devices.Environment.DeviceType != DeviceType.Device (i.e. "am I running in the Visual Studio Windows Phone 7 emulator, which has no physical accelerometer, instead of on a real device"): arrow keys synthesize a Vector3 (Left/RightX--/X++, Up/DownY++/Y--, Z fixed at -1, then Vector3.Normalize()'d). This fallback branch lives inside the same #if WINDOWS_PHONE block as the real-hardware branch (not in a separate non-#if branch the way Yacht/ SnowShovel/Bounce's own fallbacks do), which is why the earlier audit's "no #if WINDOWS_PHONE split" check missed it — that check was looking for a branch outside the #if, not a second, DeviceType-gated branch inside it. Net effect: AccelerometerSample did not need an invented keyboard scheme after all — it needed the same "un-#if an existing branch" treatment as Yacht/SnowShovel/Bounce, just with the branch boundary one level deeper (a runtime DeviceType check nested inside the #if, rather than the #if itself). See samples/AccelerometerSample/missing.md for the full account and the exact ported code. Not yet re-audited for TiltPerspective — a future session porting that sample should re-check AccelerometerHelper.cs (or equivalent) with this same correction in mind before assuming its own scheme truly must be invented from scratch.
    • ✅ AccelerometerSample (#084) ported (2026-07-10) — see samples/AccelerometerSample/missing.md.
    • Fresh correction (2026-09-01): the July port correctly recovered the original emulator keys but incorrectly made that branch the whole implementation, dropping the real ReadingChanged/Start/failure path. The fresh audit restores both original branches. It also found the actual shared gap: CNA reported DeviceType::Device in a browser even though its Web target has no sensor backend. CNA 35268971c now reports Emulator only for Web and preserves Device on desktop/mobile targets with real sensor support. Native and Wasm regressions plus unchanged-XNA device/emulator captures are retained in the SAMPLE-084 artifact. The sample is again ✅, this time with no deleted branch or sample workaround.
    • ✅ TiltPerspective (#107) ported (2026-07-10, follow-up session) — the correction above does NOT apply here; the original audit's premise was correct for this specific sample. Per this follow-up's own task brief, AccelerometerHelper.cs was re-checked against the exact same "look for a nested DeviceType branch inside #if WINDOWS_PHONE" correction found while porting AccelerometerSample, rather than assumed identical either way. Result: AccelerometerHelper.cs has no #if WINDOWS_PHONE split at all (confirmed by the .csproj's single Windows Phone build configuration and a full-file read) and its own no-hardware fallback is a non-interactive, time-driven sinusoidal wobble (FakeRollTheta += elapsed * FakeRollSpeed) — genuinely nothing keyboard/gamepad-shaped to promote, unlike AccelerometerSample. A keyboard- tilt scheme was therefore genuinely invented from scratch (NOXNA), per the 2026-07-10 user go/no-go: Left/RightX∓/, Up/Down/Y∓, Z fixed at -1, Vector3::Normalize()'d — reusing the same shape already established by AccelerometerSample's/Yacht's own promoted fallbacks for consistency, even though nothing in this sample's own original resembles it. See samples/TiltPerspective/missing.md for the full account, including a second, separate finding (confirmed via the real FNA source, not assumed): the sample's own GeometricPrimitive.cs never sets DirectionalLight0.DiffuseColor, so its balls render as near-black spheres with only a white specular highlight in both the real XNA original and this faithful port — not a CNA gap. With this sample done, both halves of the 2026-07-10 user go/no-go (task 9) are complete — see NEXT.md section 8's closing note for what a future session should look at next (nothing further is queued; the remaining candidates all need either a cna-side fix or a new user product-scope decision).
  • Orientation (#102): this sample has nothing to do with the accelerometer at all — it was miscategorized. It demonstrates GraphicsDeviceManager .SupportedOrientations/GameWindow.CurrentOrientation/OrientationChanged (screen rotation lock, not a physical sensor reading) — confirmed via full-text read of LayoutSample.cs/OrientationSample.cs: zero references to Accelerometer/Compass/any sensor class anywhere. CNA already has DisplayOrientation.hpp, and GraphicsDeviceManager/GameWindow both implement SupportedOrientations/CurrentOrientation (this is the same subsystem behind the already-fixed portrait-orientation bug — see the project's own feedback_cna_portrait_orientation_bug memory). Likely portable now, pending a full read-through to confirm no other blocker — plan.md's "Phone orientation sensor" reason for #102 is wrong and should be corrected/re-investigated, not treated as settled.
  • GeolocationSample (#095): genuinely unrelated to the accelerometer and still a real hard blocker — uses System.Device.Location.GeoCoordinateWatcher (real Windows Phone GPS/network location service), with no fallback of any kind in the original. This one's "Phone GPS hardware" reason in plan.md is accurate as-is.

Effort: S for the completed SAMPLE-084 CNA browser-classification fix. No common sensor backend work remains for #084. Historical #107 keeps its owner-approved sample deviation; #102 still needs a fresh investigation pass and Geolocation remains a separate service gap.


6. Model Asset Conversion (FBX/X → .model.json) ✅ CNA SUPPORTS STATIC MODELS

What is missing: Content.Load<Model>() IS implemented in CNA via .model.json descriptor + binary vertex/index files, and Model/ModelMesh/ModelBone/ModelMeshPart all work end-to-end for a static, rigid bone hierarchy — proven by examples/easygl_model_draw_test.cpp (2-bone model, CopyAbsoluteBoneTransformsTo, full Model::Draw chain). For any sample whose model is static (or only needs rigid parent-child bone transforms, not skeletal animation playback), the gap is purely asset conversion: XNA samples ship .x and .fbx model files that must be converted to CNA's .model.json format.

This item covers static models only. If a sample needs to play a skeletal animation (walk cycles, skinned character rigs, etc. — not just render a rigid multi-part mesh), see item 13 below instead — that capability does not exist in CNA yet, regardless of asset conversion.

Update (2026-07-11 confirmed): the multi-bone gap below is now RESOLVED. cna's develop (Task 936: ModelMesh::setParentBoneProperty(ModelBone*); Task 937: ModelTypeReader now builds one real ModelBone per mesh instead of a single synthetic "Root," both landed 2026-07-10) closes exactly the gap this caveat describes — confirmed via direct source read, not just the commit message. Caveat on the fix itself: each newly-created ModelBone's own Transform is left as Identity; getting correct per-part relative positions (not just a working Model.Bones["name"] lookup) needs each part's own real rest-transform supplied some other way, since .model.json still has no schema field for it (cna Task 938 — regenerating the asset with this repo's own already-fixed tools/fbx_ascii2model.py — remains unstarted/tracked for "whoever picks up the sample port").

Update (2026-07-11, SimpleAnimation #050 ported — first live confirmation): the multi-bone fix above was live-verified end-to-end by actually porting SimpleAnimation. tank.model.json did NOT need regenerating — the existing, unmodified, already-shipped asset (from samples/CameraShake/Content/) was reused byte-for-byte. Instead, since tank.fbx's own node hierarchy is genuinely nested (e.g. l_back_wheel_geo is a child of l_engine_geo, which is a child of the root tank_geo — confirmed via tank.fbx's own Connect: "OO" lines) and cna's reader only ever builds a flat one-level bone tree (every mesh parented directly to Root), the port's own code sets each of the 11 non-root meshes' ModelBone::Transform directly in C++ right after Content.Load<Model>(), to the correct absolute rest offset — computed by composing each part's own Lcl Translation through its real parent chain (a plain vector sum here, since every rotation/PreRotation in this asset is exactly zero, confirmed by direct read of tank.fbx). This is mathematically identical to what a real nested CopyAbsoluteBoneTransformsTo walk would produce; no cna edit, no NOXNA bypass class, no asset regeneration needed — see samples/SimpleAnimation/missing.md for the full derivation and live screenshot verification (all 4 wheels, both steer pivots, turret, cannon, and hatch confirmed rendering in correct relative position, animating correctly, across multiple frames/camera angles). SplitScreen (#076) and TankOnHeightmap (#074) can very likely reuse this same rest-transform table directly (same tank.fbx, same tank.model.json, confirmed identical asset in TankOnHeightmap's own missing.md via md5sum) — a future session porting either should start from samples/SimpleAnimation/src/Tank.hpp rather than re-deriving the transforms from scratch.

Original caveat write-up below, kept for history (the gap it describes is now fixed, see above): found while investigating SplitScreen (2026-07-04), the "asset conversion only, no CNA code changes needed" claim above only held for models with a single bone (the common case so far — Ground, tank-as-rendered-by-CameraShake, etc., which never move any part independently). examples/easygl_model_draw_test.cpp's 2-bone proof constructs ModelBones directly in C++ — it does not exercise the .model.json file format/reader's ability to build a multi-bone hierarchy from JSON. In reality, ContentManager.cpp's ModelTypeReader::Read() only ever creates one synthetic "Root" bone total; every mesh parsed from .model.json's "meshes" array is left with a null parent bone (ModelMesh doesn't even have a setter for it). So any sample needing independently-animated rigid parts (multiple named bones, one per moving mesh, XNA's common non-skinned "rig" pattern — e.g. a tank with rotating wheels/turret) is blocked on a small CNA code change, not just conversion — see samples/SplitScreen/missing.md for the full write-up (exact files/functions to change, and why samples/CameraShake/Content/tank.model.json would need zero regeneration once the reader supports it, since its mesh names already match the bone names a consumer like Tank.cs expects).

What needs to happen:

  • For single-bone (fully static) models: convert source .x/.fbx files to .model.json + binary buffers. Tools: assimp export + tools/obj2model.py, or tools/fbx_ascii2model.py. One conversion per model file per sample. No CNA code changes needed — proven twice (CameraShake, PerformanceMeasuring).
  • For multi-bone rigid-part models (SplitScreen and similar — see above): also needs a ModelMesh parent-bone setter plus a ModelTypeReader::Read() change to build one real bone per mesh (or parse an explicit bone hierarchy from the JSON) — a CNA code change, not just conversion.

Blocked samples (static geometry only, conversion-only gap): CameraShake, BloomSample (tank), Spacewar (Evolved ships/asteroids), ChaseCamera, HeightmapCollision, MarbleMaze, ShipGame, and similar.

Blocked samples (rigid multi-part bone hierarchy — needs the reader change above, not just conversion): SplitScreen (see its missing.md), SimpleAnimation, TankOnAHeightMap, CustomModelClassSample, ModelViewerDemo (all animate the same tank.fbx's wheels/turret/cannon/hatch independently). (SkinningSample, RolePlayingGame, and other skinned-animation samples are additionally blocked on item 13 regardless.)

Effort: M per model (conversion) for single-bone models, no CNA code changes needed. S–M CNA code change (once) to unblock the multi-bone rigid-part case, then M per model as above.

Tool bug found and fixed while porting LensFlare (2026-07-09): tools/fbx_ascii2model.py baked each mesh's raw Vertices:/Normals: data straight into the output buffers, ignoring the source Model node's PreRotation/ LclRotation/LclScaling/LclTranslation properties — the node transform 3ds Max (and similar DCC tools) bake in to convert their own internal axis convention into the FBX file's declared one. LensFlareSample's terrain.fbx has a PreRotation of -90,0,0 on Plane01 (a Z-up → Y-up correction); without applying it, the "terrain" came out standing on its edge (all height variation in Z instead of Y) and rendered as a flat CornflowerBlue screen with nothing visible. Fixed by parsing those properties per mesh and applying the composed rotation/scale/translation to every position and normal before building the vertex buffers (identity transform is a no-op, so any FBX with all-zero node properties, e.g. tank.fbx, converts byte-identically to before). Confirmed this does not change any already-shipped asset: every Model::<mesh> node in tank.fbx (CameraShake/CustomModelClass's source) has PreRotation 0,0,0 — but several of its meshes (the wheels/turret/ canon/hatch) do have non-zero Lcl Translation values, meaning regenerating tank.model.json with this fixed converter would now bake those parts into their correct relative positions instead of leaving them stacked at the mesh's own local origin. That regeneration was deliberately not done as part of this fix — it's a separate, pre-existing-asset change outside LensFlare's own scope, and (per the multi-bone note above) doesn't matter for CameraShake/CustomModelClass today since neither sample moves tank parts independently of each other; only relevant once a sample actually needs independent per-part motion (SplitScreen, SimpleAnimation, and the other rigid-multi-part-bone-hierarchy samples listed above), at which point regenerating tank.model.json with the fixed converter should be revisited.

Addendum found while porting PickingSample (2026-07-09): confirmed the "no per-mesh texture" gap flagged in LensFlare's missing.md ("worth a small addendum here if a future sample specifically needs a textured static model") has a much more visually severe consequence than LensFlare's own dulled/ untextured terrain. ModelTypeReader::Read()'s .model.json mesh schema has no "texture" field at all, so every mesh in PickingSample (table, Sphere, Cats, Cylinder, P2Wedge — all of which have real material textures in their source FBX files, e.g. wood.tga/cat.tga/wedge_p2_diff_v1.tga) renders with BasicEffect.TextureEnabled == false and the class-default DiffuseColor of (1,1,1) (plain white) instead of its real material color. Combined with a separate, confirmed detail in EasyGLGraphicsBackend.cpp's EnsureLit3DProgram() fragment shader — for VertexPositionNormalTexture (stride 32) draws, it unconditionally computes FragColor = texture(uTexture, vUV) * vec4(litRGB, uDiffuseColor.a) with no texture-less branch, falling back to an internal 1×1 white texture when none is bound (so the multiply is a no-op) — the combination of white DiffuseColor and XNA's standard bright EnableDefaultLighting() 3-point rig pushes litRGB above (1,1,1) for a broad range of surface normals, which OpenGL then clamps to solid white. The practical result, confirmed live via screenshot at multiple camera angles: every PickingSample model renders as a flat, fully-saturated white shape with a razor-sharp, non-antialiased silhouette and zero visible shading gradient anywhere — not merely "duller" like LensFlare's terrain, but a total loss of visual information. A correctly bound, real-world-average-brightness texture (well under 1.0 per channel) would pull the product back under the clamp for most pixels and restore normal-looking shading contrast — this is a direct, if dramatic, consequence of the existing "no per-mesh texture" gap, not an independent new lighting bug. See samples/PickingSample/missing.md for the full write-up (including confirmation, via pixel sampling across several camera angles, that this is angle-independent, unlike the separate near-plane-clipping-family bug also observed once in the same session on a different model).

Addendum found while porting HeightmapCollision (2026-07-10): confirmed a second, narrower gap in ModelTypeReader::Read() (ContentManager.cpp), independent of the texture-field gap above: it unconditionally reads every mesh's index data as std::uint16_t (idxBytes.size() / sizeof(std::uint16_t), then IndexBuffer::SetData(const std::uint16_t*, ...)) with no branch on vertex count and no "indexSize"/"indexElementSize" JSON field to request 32-bit indices explicitly. Real XNA's stock ModelProcessor automatically selects IndexElementSize.ThirtyTwoBits once a mesh exceeds 65535 vertices; a .model.json mesh that large has no way to express that today. HeightmapCollision's own procedurally-generated terrain (257×257 = 66049 vertices, from terrain.bmp) would hit exactly this limit if routed through Content.Load<Model>. Not a hard blocker in general — confirmed by direct header/source read that IndexBuffer/IIndexBufferBackend (both the EasyGL and Vulkan backends) already fully implement IndexElementSize::ThirtyTwoBits end-to-end (CreateIndexBuffer32, SetData(const std::uint32_t*, ...), and a real GL_UNSIGNED_INT glDrawElements path in EasyGLGraphicsBackend::DrawIndexedPrimitivesEx) — the gap is specifically ModelTypeReader's hardcoded 16-bit assumption, not the underlying buffer classes. Workaround used: HeightmapCollision's terrain is built directly at runtime (Terrain.hpp, NOXNA) instead of via Content.Load<Model>, using the real 32-bit IndexBuffer constructor directly — this was already necessary anyway for a different reason (no Model.Tag/custom-ContentProcessor equivalent, item #18) — so this gap didn't block that sample, but would block any future sample needing to Content.Load<Model>() a single mesh with more than 65535 vertices. Where to implement (if ever needed): ModelTypeReader::Read() (ContentManager.cpp, in the "Meshes" parsing loop, alongside the existing stride/numVertices computation) — read an optional "indexElementSize" JSON field (or just auto-select based on numVertices > 65535, mirroring real XNA's own ModelProcessor behavior) and branch to the std::uint32_t SetData overload accordingly. See samples/HeightmapCollision/missing.md for the full write-up.

Addendum (2026-07-11): SimpleAnimation's per-mesh texture gap fixed; found a real, separate bug in tools/fbx_ascii2model.py's node-transform baking that makes wholesale regeneration unsafe for this asset. SimpleAnimation (#050)'s tank rendered flat white/cream because tank.model.json (reused from CameraShake) predates cna Task 932's per-mesh "texture" field — fixed by extending tools/fbx_ascii2model.py with a generic FBX Material/Texture/Connect graph parser that emits an optional "texture" field per mesh (find_texture_relative_filenames()/ find_mesh_texture_assignments()), then hand-adding the resolved values (12 meshes, confirmed via direct FBX Connect: read: turret_alt_diff_tex for tank_geo/turret_geo/canon_geo/hatch_geo, engine_diff_tex for the other 8 parts) to the existing tank.model.jsonnot by regenerating the file wholesale. Regenerating was tested first, in a scratch directory, and found to corrupt this asset: the tool's node-transform baking (added for LensFlare's single-mesh case, unconditionally applies each mesh's own PreRotation/LclRotation/LclScaling/LclTranslation directly to its raw vertex positions) breaks down for a multi-bone, multi-mesh asset positioned via the ModelBone hierarchy (SplitScreen/SimpleAnimation/TankOnHeightmap's own approach, see this item's multi-bone addendum above) for two independent reasons, both confirmed empirically: (1) tank_geo's own FBX node has Lcl Scaling = 0.01 (every other part's is 1,1,1) — baking it directly into tank_geo's vertices shrinks the tank body to 1% size, since in the real XNA pipeline this scale is meant to live in the node's own ModelBone.Transform (applied at draw/hierarchy time, exactly like the translations this item's own ApplyRestTransforms() port-side workaround already handles), not be baked into geometry; (2) the tool bakes only each mesh's own immediate node transform, not composed through the real multi-level nested parent chain (confirmed: tank.fbx's hierarchy is tank_geo → {r,l}_engine_geo → {r,l}_{back_wheel,steer}_geo → {r,l}_front_wheel_geo, 3 levels deep) — so a regenerated r_front_wheel_geo mesh would carry only its own delta from r_steer_geo, missing r_steer_geo's and r_engine_geo's own contributions. Net effect: for any already-shipped tank.model.json-derived asset (also affects CameraShake's and CustomModelClass's own copies), do not regenerate via tools/fbx_ascii2model.py without independently re-deriving correct per-mesh rest transforms afterward — the currently-shipped vertex/index buffers (raw, un-baked local-space positions) are the ones proven correct against this repo's own ModelBone-hierarchy positioning approach. This is a cna-samples-only tooling gap (not a cna defect) — not fixed here, since fixing the tool's baking logic to compose through a real parent chain and route scale through bone transforms instead of vertex data is a real, separate task or a future session should scope explicitly rather than as a drive-by fix. See samples/SimpleAnimation/missing.md's texture-fix write-up for the full account, including the live screenshot verification and the CameraShake regression check (pixel-identical to before, confirming no cross-sample effect from this fix).


7. Audio (SoundEffect, SoundEffectInstance, Song) ✅ RESOLVED

What was missing: Audio was thought to be unimplemented but CNA fully supports audio via SDL3_mixer (SOUND_ENABLED). SoundEffect, SoundEffectInstance, Song, MediaPlayer, SoundBank, WaveBank, AudioEngine, and Cue are all implemented.

Status: Fully working. No blocker for audio samples.


8. SpriteBatch.DrawString (in-game text rendering) ✅ RESOLVED

What was missing: SpriteBatch.DrawString(spriteFont, text, position, color).

Status: Fully implemented in CNA. Depends on SpriteFont (item 2 above), which is also resolved. Use tools/make_font.py to generate font assets, then load with Content.Load<SpriteFont>("FontName").


9. Mouse Input — Viewport.AspectRatio ✅ RESOLVED (confirmed 2026-07-10)

Was: Viewport had no AspectRatio convenience property; computed manually as (float)Width / (float)Height in each sample.

Now: confirmed via direct source read while porting MarbleMaze (2026-07-10) that Viewport::getAspectRatioProperty() already exists and is implemented (cna/include/Microsoft/Xna/Framework/Graphics/Viewport.hpp:54 and the matching .cpp). Used directly in samples/MarbleMaze/src/Objects/Camera.hpp's Initialize() — no manual (float)Width/Height workaround needed. Per this repo's own "risky assumption" caveat (NEXT.md section 5), this item had gone stale silently; marked resolved now that it's been re-verified live.

Where implemented: cna/include/Microsoft/Xna/Framework/Graphics/Viewport.hpp

Effort: — (done)


10. GamePad.IsButtonDown shortcut

What is missing (minor): In XNA, GamePad.GetState(PlayerIndex.One).Buttons.Back is a direct member access. In CNA, Buttons is accessed via getButtonsProperty() and Back via getBackProperty(). GamePadState.IsButtonDown(Buttons::Back) works correctly and is used in the ports.

Where to implement: Not strictly needed; IsButtonDown is the cleaner API anyway.

Effort:


11. Custom User Effect Shader Conversion (HLSL .fx → .shader.json) ✅ CNA SUPPORTS CUSTOM EFFECTS

What is missing: Content.Load<Effect>() IS implemented in CNA via .shader.json descriptor referencing GLSL vertex + fragment shader files. The gap is shader conversion: XNA samples use HLSL .fx files that must be rewritten as GLSL and described via .shader.json.

What needs to happen:

  • Translate HLSL .fx shader logic to GLSL (vertex + fragment)
  • Create .shader.json descriptor per effect
  • Tools: manual port or SPIRV-Cross + dxc pipeline

Blocked samples: BloomSample (3 shaders), DistortionSample, NonPhotoRealistic, NormalMapping, PerPixelLighting, VertexLighting, RimLighting, ShadowMapping, ShatterEffect, Particles3D, and all Phase 3+ shader samples.

Effort: XL


12. RenderTarget2D ✅ RESOLVED

What was missing: RenderTarget2D and GraphicsDevice.SetRenderTarget() were thought to be missing but are fully implemented in CNA (RenderTarget2D.hpp / .cpp, backend framebuffer support in both EasyGL and Vulkan backends).

Status: Fully working. No blocker.


13. Skeletal Animation Playback — ✅ NO LONGER A BLANKET SAMPLE BLOCKER

Corrected 2026-08-30 by SAMPLE-051 and SAMPLE-054. This item mixed two different things:

  1. an optional CNA-specific open .model.json skeletal schema and conversion tool; and
  2. the behavior the original XNA samples actually use: official pipeline XNB models carrying skinned vertex channels, SkinnedEffect, and sample-owned animation data in Model.Tag.

The second path works end to end. SAMPLE-051 (CustomModelAnimation) and the canonical SAMPLE-054 (SkinningSample) both compile their unchanged custom processors with the official XNA 4.0 pipeline and load those exact XNBs in CNA. The sample-owned AnimationClip, Keyframe, AnimationPlayer and SkinningData classes are ported as sample code, not invented as XNA framework APIs. CNA's generic AOT reflective readers replace only the .NET reflection over those sample-owned fields.

SAMPLE-054 proves the complete base path: reflective SkinningData in Model.Tag, bind pose, inverse bind pose, parent hierarchy, chronological keyframes, skinned vertex indices/weights, SkinnedEffect, textures and rendering. At two pinned clip times CNA and real XNA agree within 8 color levels on 99.91–99.95 % of all pixels and have exact foreground bounds; native and WEBGL2 both animate normally. See samples/SkinningSample/{missing,diff}.md.

SAMPLE-055 independently proves the Part 2 extension path: a processor-built named-bone dictionary, manual local-bone overrides between the three animation stages, a rigid model attached to an animated hand and reflectively loaded SkinnedSphere[] collision volumes. XNA and CNA again have exact foreground bounds and 99.92–99.95 % agreement within 8 levels; native and WEBGL2 render the same moving wireframe spheres. The only newly exposed framework omission was the stock closed DictionaryReader<string,int> registration, repaired generally rather than in the game. See samples/SkinnedModelExtensions/{missing,diff}.md.

SAMPLE-056 proves the distinct CPU-skinning path. Its unchanged custom processor/writer produces a CpuSkinnedModel containing bind-pose positions/normals, four indices and four weights per vertex, an index buffer and shared BasicEffect. The port reads that exact XNB and performs the original 12-field weighted matrix blend, position/normal transforms and discard-mode dynamic vertex-buffer upload on the CPU every frame; it does not substitute the parallel SkinnedEffect model. At two pinned times the CPU path agrees with real XNA on 99.99% of model pixels within eight color levels, and the CPU/GPU silhouettes have identical bounds. See samples/CPUSkinning/{missing,diff}.md.

The CNAEXT Microsoft::Xna::Framework::Graphics::AnimationPlayer and the proposed open-format schema remain separate optional engine features. Their incomplete .model.json loader/converter does not justify replacing or blocking an original sample that uses official XNB content.

Remaining rule: retest RolePlayingGame and any other animation sample against their own source and reader tables. They may expose different bounded or large gaps, but this historical item is no longer evidence that they are blocked.

Blocked samples: none by blanket inference.

Effort: no sample-campaign task remains here; open-format tooling, if desired, is a separate CNAEXT product decision.


16. Microphone capture (Microsoft.Xna.Framework.Audio.Microphone) ✅ RESOLVED

Corrected 2026-07-06: this item was added in the same session that wrote samples/MicrophoneEcho/missing.md, based on general SDL3 capability knowledge rather than an actual check of cna's current source — a live check the same day found Microphone fully implemented (src/Microsoft/Xna/Framework/Audio/ Microphone.cpp, 252 lines, include/.../Audio/{Microphone,MicrophoneState, NoMicrophoneConnectedException}.hpp, real tests), merged via feature/audio into develop on 2026-07-04 (commits like "wire Microphone::GetData()/GetQueuedBytes() to the real stream") — two days before this item was written. No CNA gap remains for MicrophoneEcho (#098); update samples/MicrophoneEcho/missing.md and re-attempt the port.

Effort: — (done in cna)


17. Multiplayer networking (Microsoft.Xna.Framework.Net / GamerServices.NetworkSession) ✅ RESOLVED

Corrected 2026-07-06: this item was added in the same session that wrote the NetRumble/ClientServerSample/NetworkPrediction/PeerToPeer placeholders, based on the C# originals' API usage without actually checking cna's current source for whether it already implements GamerServices/Net — a live check the same day found a full, real implementation: include/Microsoft/Xna/Framework/Net/{ NetworkSession,NetworkSessionType,NetworkSessionProperties,AvailableNetworkSession, AvailableNetworkSessionCollection,...}.hpp, NetworkSession.cpp (836 lines), GamerServices/{GamerServicesComponent,AvatarRenderer,...}, real LAN discovery via CNA::Internal::Net::ENetDiscoveryService, and NetworkSessionType including SystemLink (exactly what these 4 samples use) — merged via feature/net into develop on 2026-07-04, two days before this item was written.

No CNA networking gap remains for ClientServerSample (#091), NetworkPrediction (#100), or PeerToPeer (#103) — update their missing.md files and re-attempt porting. NetRumble (#062) is now only single-blocked, by item 11 (its 4 custom .fx shaders, including the bloom post-process) — update its missing.md to drop the networking half of its "double-blocked" framing.

Update, same day, after actually porting ClientServerSample: "the types exist and are tested" turned out to be true but incomplete — three more specific, narrower gaps surfaced only once a real sample was built and run against them, each documented as its own item since they're independently fixable: item 19 (GamerServicesDispatcher::Update() no-op hangs the synchronous Create/Find/ Join wrappers whenever a GamerServicesComponent is present — i.e., whenever a sample matches its own C# original's real usage), item 20 (NetworkGamer.IsHost/ .Id are hardcoded stub constants, not per-instance state), and item 21 (the initial GamerJoined event is queued for the next frame instead of raised synchronously during Create()/Join(), unlike real XNA). ClientServerSample works around all three at the sample level (see its missing.md) and is fully ported and live-verified; the other three networking samples likely need the same workarounds but haven't been individually re-confirmed yet. Lesson reinforced: even a "the API surface exists and has tests" confirmation isn't the same as "a real caller integrating it works" — building and running an actual sample against new API surface can surface gaps that isolated unit tests didn't exercise.

Effort: — (done in cna) for the core types; see items 19–21 for the narrower gaps found integrating them.


18. Content-pipeline processor extensibility (build-time ContentProcessor chaining) — sample blocker resolved

What is missing: CNA's entire asset story is "convert once, offline, with a standalone tool, into a static runtime JSON/binary format" (tools/obj2model.py, tools/make_font.py, tools/gen_help_png.py, etc.). There is no pluggable, MSBuild-time, C#-ContentProcessor-style extensibility point — nothing resembling XNA's ContentProcessor<TInput,TOutput> / ContentProcessorContext.Convert/BuildAsset chaining model, where a sample can supply its own processor(s) that run as part of the content build and transform/synthesize data before it ever reaches the runtime (e.g. baking a reflection cubemap from a flat photo, or flattening/re-deriving model data in a project-specific way).

This is a different, deeper class of gap than items #6 (static model format conversion) or #11 (custom shader conversion) — both of those assume an offline tool already produced a static input file; this item is about the meta-capability of a custom build-time transform pipeline itself.

Sample-blocker resolution (SAMPLE-053, 2026-08-30): CustomModelEffect's unchanged three-processor assembly was compiled and run by the official XNA 4.0 pipeline. Its exact Reach .xnb outputs are retained in the port, including the compiled effect, processed model, diffuse texture and generated DXT1 cubemap. This is the campaign's approved externally-produced asset path and preserves the runtime behavior exactly; no one-off Python/C++ processor, loose model or sample-side shader was invented. The real integration exposed a CNA runtime defect instead: ExternalReferenceReader forced every type-erased reference to Texture2D, which failed on the pipeline's TextureCube. That was fixed generally in cnanext XNB-35A and verified by a focused test plus the live sample. See samples/CustomModelEffect/missing.md.

The general authoring capability described above is still not implemented. It is no longer a blocker for any audited sample and should not be built speculatively; reopen it only if the owner requests a CNA-native processor SDK or a future sample cannot use its official pipeline output.

Effort: L for a future general authoring feature; no remaining sample work is assigned here.


19. NetworkSession::Create/Find/Join hang forever when a GamerServicesComponent is added ✅ RESOLVED

Resolved 2026-07-06 in cna's feature/net (commit 08171ac, Task 12.1): NetworkSessionAction now completes the instant it's constructed (every Begin* already does all its real work synchronously in End*, so there's no genuine pending operation to wait on) — confirmed this hang is a real bug in FNA's own reference source too (GamerServicesDispatcher.Update() is equally an empty no-op there), not just a CNA porting defect. ClientServerSample's workaround (omit GamerServicesComponent) was removed the same day; the component is now constructed and added normally, matching the C# original, and confirmed live (no hang, real GamerServicesDispatcher::Initialize() now populates Gamer::SignedInGamers — see samples/ClientServerSample/missing.md's Verification section). NetworkPrediction (#100)/PeerToPeer (#103) can now add GamerServicesComponent normally too when ported; not yet separately re-confirmed for those two.

What is missing: confirmed live (2026-07-06) while porting ClientServerSample (#091): calling NetworkSession::Create(...) after constructing a GamerServicesComponent and adding it to Game.Components — exactly what every one of these samples' C# originals do in their constructor (Components.Add(new GamerServicesComponent(this));) — hangs forever in a tight busy-loop, 99% CPU, no output, unresponsive to SIGTERM. Root cause, confirmed by reading the source directly: NetworkSession::Create()'s synchronous wrapper is

System::IAsyncResult* result = BeginCreate(...);
while (!result->getIsCompletedProperty())
{
    if (!GamerServices::GamerServicesDispatcher::UpdateAsync())
        activeAction_->setIsCompletedProperty(true);
}
return EndCreate(result);

GamerServicesDispatcher::UpdateAsync() is { if (isInitialized_) Update(); return isInitialized_; }, and GamerServicesDispatcher::Update() is a completely empty function body — it does nothing at all, ever. BeginCreate() never sets IsCompleted itself (it just constructs a NetworkSessionAction and returns). So: with no GamerServicesComponent (dispatcher never initialized), UpdateAsync() returns false on the very first loop iteration, which forces IsCompleted = true, and the loop exits immediately (this is why CNA's own NetworkSessionTests.cpp — which never constructs a GamerServicesComponent — doesn't hit this). But with a GamerServicesComponent added (matching every sample's own C# original), isInitialized_ becomes true, UpdateAsync() unconditionally returns true forever, and nothing — nowhere in the codebase — ever sets IsCompleted on that pending action. The loop never exits.

Where to implement: cna/src/Microsoft/Xna/Framework/GamerServices/GamerServicesDispatcher.cpp's Update() needs to actually do something when a NetworkSession action is pending — at minimum, poll the ENet backend for whether the requested operation (host bind, LAN session discovery, join handshake) has completed and call activeAction_->setIsCompletedProperty(true) once it has, mirroring what a real synchronous completion would look like.

Workaround used in ClientServerSample (#091): don't construct/add a GamerServicesComponent at all. Confirmed this loses no other functionality in this CNA implementation (the component has no other observable effect — Guide.ShowSignIn is independently a no-op regardless of whether the component exists, per item 15- adjacent findings). This is a real, documented deviation from the C# original (see samples/ClientServerSample/missing.md), not a silent workaround.

Blocked samples: ClientServerSample (#091, workaround applied, ported), and presumably NetworkPrediction (#100), PeerToPeer (#103), NetRumble (#062) — all four call NetworkSession::Create/Find/Join the same synchronous way and all four C# originals add a GamerServicesComponent; the same workaround (omit it) should apply, but hasn't been separately confirmed live for the other three yet.

Effort: M — GamerServicesDispatcher::Update() needs real logic, not a bigger architectural change.


20. NetworkGamer identity is not distinguishable (IsHost always true, Id always 0) ✅ RESOLVED

Resolved 2026-07-06 in cna's feature/net (commit 81f10b5, Task 12.2): NetworkGamer gained real SetId/SetIsHost (NOXNA), wired through NetworkSession's constructor (host vs. joined-client state) and through ENetBackend.cpp's already-existing real, cross-machine-consistent wire-id system (it just wasn't surfaced through the public API before this fix). ClientServerSample's bool isHost_ local-tracking workaround was removed the same day; every call site now uses networkSession_->getIsHostProperty()/gamer->getIsHostProperty() directly, confirmed live for the solo-host case (see samples/ClientServerSample/missing.md's Verification section). Scoped, still-open limitation, tracked here rather than as a new item: a remote gamer representing the actual host machine still reports IsHost == false as seen from a client (RosterEntry carries no host flag on the wire) — a strict improvement over "every gamer said true," not a full fix; would need a wire-protocol addition to close. Genuine multi-gamer Id-routing within ClientServerSample itself wasn't re-verified live this session (blocked by an unrelated, pre-existing ENetDiscoveryService two-process discovery limitation on this container — see missing.md) but is proven at the cna test-suite level (NetworkSessionTests.cpp, ENetBackendTests.cpp, TwoProcessLoopbackTest.cpp).

What is missing: confirmed by reading cna/src/Microsoft/Xna/Framework/Net/ NetworkGamer.cpp directly:

bool NetworkGamer::getIsHostProperty() const              { return true; }
SharpRuntime::bytecs NetworkGamer::getIdProperty() const  { return 0; }

Both are hardcoded stub constants, not derived from any actual state (doc comments call this "matching FNA's stub" — plausible for real FNA/XNA without a live Xbox LIVE/GfWL backing service, but it means every NetworkGamer in a session — host and every client — reports IsHost == true and Id == 0, with no way to tell them apart via these two properties. Consequences, both confirmed by reading the code (not merely inferred):

  • NetworkSession::getIsHostProperty() is implemented as "true if any local gamer's IsHost is true" — since that's unconditionally true, NetworkSession.IsHost is also always true, on every machine, host or client.
  • NetworkSession::FindGamerById(id) does a linear getIdProperty() == id scan — since every gamer's Id is 0, this always returns the first gamer in AllGamers, regardless of which id was actually requested. Any protocol that writes gamer.Id into a packet and looks it back up on the receiving end (e.g. ClientServerSample's/NetworkPrediction's/PeerToPeer's per-tank state sync) will misroute every gamer's state onto the first gamer whenever more than one gamer is in the session.

Workaround used in ClientServerSample (#091): for "am I the host" (session-level), track a local bool isHost_ set explicitly at the call site — true after NetworkSession::Create(), false after NetworkSession::Join() — since each machine always knows unambiguously which one it called, this fully replaces the broken NetworkSession.IsHost/gamer.IsHost for this sample's needs. No workaround was applied for FindGamerById/Id — the port keeps the original's gamer.Id-based wire protocol as-is (matching the C# source), so multi-gamer sessions will not correctly route tank state past the first gamer until this is fixed in cna; this is a known, documented, unfixed limitation for that one scenario (see samples/ClientServerSample/missing.md). Single-gamer (solo host, no other clients joined) sessions are unaffected and were live-verified working.

Where to implement: NetworkGamer needs a real per-instance identity — e.g. an isHost_ bool member correctly set at construction (mirroring NetworkSession:: host_, which is tracked correctly) and returned by getIsHostProperty() instead of a hardcoded true; and a real per-session-unique byte id assigned when each gamer joins (local or remote) and returned by getIdProperty() instead of a hardcoded 0.

Blocked samples: ClientServerSample (#091, IsHost worked around, Id/ FindGamerById not), and likely NetworkPrediction (#100), PeerToPeer (#103), NetRumble (#062) to varying degrees depending on whether each protocol relies on gamer.Id/FindGamerById for multi-gamer state routing — not separately confirmed for those three yet.

Effort: M.


21. Initial GamerJoined event is queued for the next Update(), not raised synchronously during Create()/Join() ✅ RESOLVED

Investigated 2026-07-06 in cna's feature/net (Task 12.3) — initially found not to have a simple fix. Traced against this exact sample's real C# reference source (ClientServerGame.cs) and confirmed both originally-proposed fix approaches below could not actually work, and either would have been an active regression: subscription (HookSessionEvents()) always happens strictly after Create()/Join() already returned control to the caller (there is no way to subscribe any earlier — the session pointer doesn't exist yet), so raising the event any earlier than that fires into zero subscribers — including inside the constructor or inside Create()/Join()'s own wrapper before returning. Worse, doing so would have broken ClientServerSample's own already-working, live-verified workaround below, which depended on the event still being queued (not yet fired into a void) at the point it calls Update() right after subscribing.

Real XNA's NetworkSession.GamerJoined is documented to replay itself immediately upon += subscription for every gamer already present in the session — a "hot event with backlog replay" semantic that CNA's System::EventHandler<T> (a separate sharp-runtime repo, not cna itself) had no hook for.

Resolved same day, after the user reviewed this exact analysis and approved a sharp-runtime change: EventHandler<T> gained a generic, opt-in SetReplayHook() (sharp-runtime develop, commit 69661c2) — every other EventHandler<T> in the codebase that never calls it behaves exactly as before, so this isn't the one-off/non-uniform special case that was ruled out above. NetworkSession's constructor (cna's feature/net, commit ab05395) now sets this hook so GamerJoined += handler immediately replays for every gamer already in the session, matching real XNA. ClientServerSample's networkSession_->Update(); workaround (right after HookSessionEvents() in both CreateSession()/ JoinSession()) was removed and confirmed live to still work correctly with no crash (see samples/ClientServerSample/missing.md's Verification section for the full account, including how xdotool input unreliability was ruled out as the cause of anything via a controlled debug-auto-trigger test). See cna's plan_net.md Task 12.3 for the full investigation and fix.

What is missing: in real XNA, NetworkSession.Create()/.Join() synchronously establish the initial local gamer(s) and raise GamerJoined for each one as part of that same call — by the time Create() returns, code relying on GamerJoined having already fired (e.g. a Tag set by the handler) can safely assume it's done. CNA's NetworkSession constructor instead queues a GamerJoin NetworkEvent per initial gamer into an internal std::queue, which is only drained by NetworkSession::Update() — i.e., not until the next frame's networkSession. Update() call in the ported sample's own game loop, one full Update() cycle after Create()/Join() returns.

Consequence, confirmed live: a sample whose Update() loop is structured like the original (read/act on each local gamer via its Tag before calling networkSession.Update() — exactly ClientServerSample's UpdateNetworkSession() shape) will find an empty Tag (std::any holding nothing) on the very first network-session frame, since the handler that populates it hasn't run yet. Observed as an uncaught std::bad_any_cast terminating the process.

Workaround used in ClientServerSample (#091): call networkSession_->Update(); once, immediately after HookSessionEvents(), inside both CreateSession() and JoinSession() — this drains the queued initial GamerJoin event(s) synchronously before control returns to the normal per-frame loop, matching what real XNA does implicitly. Confirmed live: with this fix, session creation + tank spawn + render all work end-to-end with no crash (screenshot-verified).

Where to implement: NetworkSession's constructor could raise GamerJoined directly for each initial local gamer instead of only queuing a NetworkEvent, or Create()/Join()'s synchronous wrappers could drain the queue once before returning — either would remove the need for every calling sample to work around it.

Blocked samples: ClientServerSample (#091, workaround applied, ported); likely also affects NetworkPrediction (#100) and PeerToPeer (#103) if they have a similar "read local gamer state before pumping the session" loop shape — not separately confirmed for those two yet.

Effort: S.


22. EasyGL backend ignores BlendState.ColorWriteChannels (color-write mask never applied)

Found while porting LensFlare (2026-07-09). LensFlareComponent's occlusion-query trick (LensFlareComponent.cs's UpdateOcclusion()) relies on a custom BlendState with ColorWriteChannels = ColorWriteChannels.None so the query polygon it draws to test sun visibility never actually appears on screen — only its occluded/visible pixel count matters. Confirmed via direct source grep: grep -rn "ColorWriteChannels\|glColorMask" src/CNA/Internal/Backends/EasyGL/ in cna returns zero matches — the EasyGL backend parses/stores BlendState.ColorWriteChannels (and ...Channels1/2/3) but never calls the OpenGL-ES equivalent (glColorMask) to actually apply it, so every draw call writes all four color channels regardless of the active BlendState.

Confirmed live: running the ported sample shows a solid opaque white square (the occlusion-query polygon, querySize × querySize at the current light-screen position) that should be invisible, on top of the CornflowerBlue background and the already-known near-plane-clipping thin-line artifact (item 5's terrain-asset variant — same shared framework cause as the tank models, confirmed by the terrain now converting/orienting correctly per item 6's write-up above, so the thin line is not an asset problem).

Root cause: CNA::Internal::Backends::EasyGL::EasyGLGraphicsBackend has no glColorMask call anywhere in its state-application path.

Where to implement: wherever the EasyGL backend applies BlendState to GL state (alongside its existing blend-func/blend-equation setup) — add a glColorMask call driven by BlendState.ColorWriteChannels (all four channels come from the same ColorWriteChannels value unless IndependentBlendEnable/per-target overrides are also unimplemented, which is a separate, likely-also-missing MRT feature not investigated here).

Blocked samples: LensFlare (#041, ported; ported using the assumption that fixing this is a follow-up, not a blocker for shipping the port — see its missing.md). Any future sample using a ColorWriteChannels.None/partial-channel BlendState for a similar depth-only or stencil-only trick would hit the same gap.

Effort: S.


23. Game::DoInitialize() wires up Components_.ComponentAdded after calling the user's Initialize() override — CORRECTED (2026-07-11): not a CNA bug

Reconciled in cna's develop (Task 929, landed 2026-07-10): investigated directly against real FNA source and found this item's own root-cause claim below was factually wrong — FNA has the identical subscribe-after-Initialize() ordering, not the "subscribes immediately in the constructor" behavior this item originally asserted. So cna's behavior already matches real XNA/FNA; there is no defect to fix here. Graphics3D's/PickingSample's own AddComponent() workaround (explicit component->Initialize() call after Components.Add()) remains the correct thing to do for a component added during Initialize() — it's accommodating a real XNA/FNA ordering gotcha every original C# sample using this pattern would also have to work around (typically by adding components from the constructor instead, which is what most XNA samples do in practice), not compensating for a CNA-specific defect. No cna code changed. The workaround stays in every sample that uses it; no regression risk since nothing was modified.

Original write-up below, kept for full history of the (incorrect) root-cause theory and the workaround, which remains valid regardless:

Found while porting Graphics3D (2026-07-09). Real XNA/FNA's Game constructor subscribes to Components.ComponentAdded/ComponentRemoved immediately, before any user Initialize() override can run — this is why real XNA supports the common pattern of creating DrawableGameComponents and calling Components.Add(...) from inside Initialize() (not just the constructor), which is exactly what Graphics3D's C# original does (GameMain.cs's CreateLightEnablingButtons() etc., all called from Initialize()).

Confirmed live: cna's Game::DoInitialize() (src/Microsoft/Xna/Framework/Game.cpp) calls Initialize() first, and only wires up Components_.ComponentAdded/ComponentRemoved afterward (DoInitialize()'s own body, after the Initialize() call). A component added to Components from within Initialize() is added to the collection (and later categorized as updateable/drawable, so it does run every frame) but its own Initialize() — and therefore LoadContent(), since DrawableGameComponent::Initialize() calls it — is never invoked, since the event that would trigger it isn't subscribed yet. Reproduced directly: a Checkbox (DrawableGameComponent) added this way had an unset std::optional<SpriteBatch>/std::optional<Texture2D> at first Draw() — segfault on the very first frame.

Root cause: Game::DoInitialize() subscribes the ComponentAdded/ ComponentRemoved event handlers after calling Initialize(), not before — deviates from FNA/XNA's own Game constructor, where the equivalent subscription happens immediately.

Where to implement: move the Components_.ComponentAdded +=/ ComponentRemoved += subscription earlier — either into Game's constructor (matching FNA exactly) or at minimum to before the Initialize() call inside DoInitialize().

Workaround used in Graphics3D: an AddComponent(Checkbox*) helper that calls Components.Add(component) followed by an explicit component->Initialize(). Safe even after a cna fix, since DrawableGameComponent::Initialize() already guards against double-initialization.

Blocked samples: Graphics3D (workaround applied, ported). PickingSample (#047) hit the identical shape and applied the identical workaround. Every other sample in this repo happens to add its components from the constructor (before Initialize() runs at all), which sidesteps this gap entirely. Any future sample following the same pattern (matching a C# original that does its own component creation inside Initialize()) would hit the same bug.

Addendum found while porting TrianglePicking (2026-07-09): the precise trigger condition is narrower than "added from Initialize() vs. the constructor" — it's about ordering relative to the user override's own call to Game::Initialize()/base.Initialize(). cna's base Game::Initialize() (Game.cpp, not DoInitialize()) separately, unconditionally loops over every component already present in Components_ at the time it runs and calls each one's own Initialize() directly — entirely independent of whether ComponentAdded has been subscribed yet. Graphics3D's and PickingSample's own C# originals both add their component(s) from inside Initialize() before that override's own call to base.Initialize(), so the base loop's snapshot doesn't include them yet (the loop already ran) and only the — not-yet- subscribed — ComponentAdded event could have caught them. TrianglePicking's own C# original instead adds its Cursor from the constructor, i.e. long before Initialize()/base.Initialize() runs at all — confirmed live, this needs no workaround: by the time either Game::Initialize() (base or override) executes, the component is already present in Components_, so the base loop's unconditional pass initializes it correctly with no dependency on ComponentAdded timing at all. In short: constructor-time adds are safe (base Game::Initialize()'s own loop catches them); Initialize()-time adds are only safe if they happen after that override's own base.Initialize() call (untested by any sample so far — every sample hitting this pattern in C# calls base.Initialize() last, matching real XNA convention). See samples/TrianglePicking/missing.md for the full account.

Effort: S.


24. GraphicsDevice::Clear(Color) (single-argument overload) never clears the depth buffer ✅ RESOLVED (2026-07-11 confirmed)

Resolved in cna's develop (Task 928, landed 2026-07-10): Clear(const Color&) now also clears depth/stencil, matching FNA. Per cna's own Task 928 notes the fix is "behaviorally inert" in practice on both backends (each already cleared depth by other means), but it's a real FNA-parity correctness fix and removes this as a latent risk for any future sample relying on cross-frame depth-buffer reuse (e.g. ReachGraphicsDemo's EnvmapDemo, which already worked around this at the sample level with the 2-argument Clear(Color, float) overload — that workaround is now redundant but harmless).

Found while porting Graphics3D (2026-07-09), while investigating that sample's invisible-model bug (item 5's near-plane-clipping-family entry — this turned out not to be the cause of that specific bug, but is a real, separate gap). Real XNA's GraphicsDevice.Clear(Color) convenience overload clears the color target, depth buffer, and stencil buffer together (documented behavior, matches FNA).

Confirmed via direct source read (src/Microsoft/Xna/Framework/Graphics/ GraphicsDevice.cpp): the single-argument Clear(const Color&) overload only ever calls backend_->Clear(r,g,b,a) (color only) — it never touches depth or stencil. The Clear(ClearOptions, const Color&, float depth, int stencil) and Clear(const Color&, float depth) overloads both correctly clear depth (ClearColorAndDepth) when asked; only the plain single-Color-argument overload is missing it.

Root cause: GraphicsDevice::Clear(const Color&) doesn't forward to the ClearOptions-based overload with DepthBuffer included — it's a strict subset of what real XNA's same-signature overload does.

Impact: every 3D sample calling device.Clear(SomeColor) at the top of its own Draw() (i.e. essentially every 3D sample in this repo — CameraShake, CustomModelClass, LensFlare, Graphics3D) draws each frame against a depth buffer that was never actually cleared by that call, relying on whatever the driver/backend leaves behind from the previous frame (or an uninitialized buffer on the first frame). Tested substituting the two-argument Clear(color, 1.0f) overload in Graphics3D specifically to see if this explained that sample's invisible spaceship — it didn't change the result, so this is confirmed independent of item 5's near-plane-clipping bug, not a duplicate finding.

Where to implement: GraphicsDevice::Clear(const Color& color) in GraphicsDevice.cpp should forward to Clear(ClearOptions::Target | ClearOptions::DepthBuffer | ClearOptions::Stencil, color, 1.0f, 0) (or equivalent), matching real XNA's documented behavior for this overload.

Blocked samples: none outright (every affected sample still renders something, since GPU depth buffers are typically cleared to a sane default by the driver on allocation in practice) — but this is a latent correctness gap that could cause intermittent, driver-dependent depth artifacts in any 3D sample using this Clear overload, and is worth fixing on general principle.

Effort: S.


25. VertexBuffer/IndexBuffer have no GetData() (no readback of GPU buffer contents) ✅ RESOLVED (2026-07-11 confirmed)

Resolved in cna's develop (Task 930, landed 2026-07-10): both classes gained a real GetData<T>(), CPU-shadow-buffer based, covering all vertex types and both 16-/32-bit index widths. TrianglePicking's own tool-level --picking sidecar workaround (below) still works and was not revisited, but a future sample needing readback from a Model it did not itself convert can now use the real API directly instead of needing a new offline-tool flag.

Found while porting TrianglePicking (2026-07-09). This sample's C# original (TrianglePickingSample) needs its models' raw triangle vertex positions at runtime for a per-triangle ray-intersection test — real XNA gets this via a custom content-pipeline processor (TrianglePickingProcessor, a ModelProcessor subclass) that walks the model's node tree at content-build time and attaches a flat Vector3[] (plus a precomputed BoundingSphere) to Model.Tag. The task brief for this port suggested an alternative if that path weren't available: read the data back from the model's already-loaded VertexBuffer/IndexBuffer at runtime instead (real XNA's VertexBuffer.GetData<T>()/IndexBuffer.GetData<T>()).

Confirmed via direct, full read of both headers (cna/include/Microsoft/Xna/ Framework/Graphics/{VertexBuffer,IndexBuffer}.hpp): neither class has any GetData method at all — every data-transfer method on both classes is a SetData/ SetDataRaw/SetDataWithOptions upload path (grep confirms zero occurrences of GetData in either file). This means there is currently no way to read back vertex or index data from a GPU buffer in CNA, for any purpose — not just an inconvenience specific to this sample's Model.Tag gap (DEFERRED.md item #18, custom ContentProcessor extensibility), but a distinct, narrower missing capability: even a sample willing to re-derive its own picking data at runtime from an already-loaded Model's buffers has no API surface to do so.

Where to implement: add GetData<T>(T* data, int count) (and the startIndex/ elementCount overloads, matching the existing SetData overload set) to both VertexBuffer and IndexBuffer — reading back from the backend's GPU buffer object (glGetBufferSubData on the EasyGL/OpenGL-ES backend; note OpenGL ES 3.x does support glGetBufferSubData, unlike some other ES-vs-desktop-GL gaps this repo has hit, so this should not need a new capability probe). CNA::Internal::Backends:: IVertexBufferBackend/IIndexBufferBackend would need a matching virtual method added for each backend (EasyGL, Vulkan) to implement.

Workaround used in TrianglePicking (#048): since CNA's whole asset story is "convert once, offline, into a static runtime format" (item #18's framing) rather than XNA's build-time ContentProcessor chaining, this port extends tools/ fbx_ascii2model.py (which already parses every mesh's raw triangle/vertex data to produce .model.json's own _verts.bin/_idx.bin files) with an optional --picking flag that, from that same already-parsed data, additionally emits a flat binary sidecar file (<Model>_picking.bin: triangle-expanded float32 x,y,z positions, no normal/uv, 9 floats per triangle) — this is generated once, offline, at asset- conversion time, and the C++ port just reads it directly from disk at LoadContent() time (see samples/TrianglePicking/src/TrianglePickingData.hpp). This sidesteps the gap entirely rather than working around it inside CNA, and needed no changes to ModelTypeReader/Model.Tag-equivalent machinery. A future sample that specifically needs to read back data from a Model it did not convert itself (e.g. a third-party/pre-existing .model.json with no matching source FBX available at runtime) would still need the real GetData() fix above.

Blocked samples: none outright — the tool-level workaround fully unblocks TrianglePicking (#048). Any future sample needing genuine runtime readback of GPU buffer contents (not just triangle data producible by this repo's own offline converters) would hit this gap for real.

Effort: S–M (mirrors the existing SetData overload set; the GPU-side readback call itself is a single line per backend on both EasyGL and Vulkan).


26. ModelTypeReader::Read() uploads corrupted vertex data for every stride-32 .model.json (vtable-size mismatch) — likely the true cause of the "near-plane-clipping" bug family ✅ RESOLVED (2026-07-11 confirmed)

Resolved in cna's develop (Task 927, landed 2026-07-10). Confirmed by direct source read: ModelTypeReader::Read() (ContentManager.cpp) no longer compares the declared "vertexStride" against sizeof() of the polymorphic, vtable-carrying Graphics::VertexPositionXxx structs — it now reads each vertex's fields at hardcoded clean XNA offsets (16/20/24/32) and constructs real objects field-by-field, the same general shape this repo's own bypasses (CylinderModel.hpp/RawModel.hpp/RawMesh.hpp) already used. Confirmed live (2026-07-11): rebuilt the full aggregate project against current cna develop HEAD (0 errors/0 warnings) and ran CameraShake_cna_samples (which already used plain Content.Load<Model>("tank")/Content.Load<Model>("ground"), no bypass) under SDL_VIDEODRIVER=x11 — the tank, previously invisible/reduced to a thin line at this sample's camera distance, now renders as a complete, correctly-shaped solid silhouette. (It renders flat white, not textured — a separate, already-documented, pre-existing characteristic: tank.model.json predates Task 932's per-mesh "texture" field, so it has no texture entry to bind; unrelated to this item.)

Fresh-audit cleanup (2026-08-30): SAMPLE-057 InverseKinematics now uses the exact official cylinder.xnb through plain Content.Load<Model>("cylinder"); the old CylinderModel.hpp, raw buffers and converted model JSON were deleted. Native OPENGLES3 matches XNA at 99.90% of pixels within eight levels and WEBGL2 at 99.86%, both 100% after a four-pixel blur. This directly verifies the repaired stock XNB model path for the sample.

Not yet done (future cleanup, optional, not blocking anything): the already-shipped bypass code in ChaseCamera/MarbleMaze/ReachGraphicsDemo/RimLighting is no longer strictly necessary and could be replaced with plain Content.Load<Model>() for closer fidelity to each sample's C# original (per this repo's own porting philosophy of staying as close to the original as the language allows) — not verified per-sample live, and not done as part of this documentation update; a future session should treat this as a real but low-priority simplification opportunity, not a bug.


26 (original write-up, kept for full history)

Found while porting InverseKinematics (2026-07-10). A straightforward first port of this sample's cylinder.x (converted via assimp export + tools/obj2model.py, the exact same pipeline already proven for CameraShake/PerformanceMeasuring/Graphics3D) built and loaded without error — confirmed via debug instrumentation: Content.Load<Model> ("cylinder") produced exactly 1 mesh, 1 ModelMeshPart (418 vertices, 190 primitives), and a correctly-linked BasicEffect — but the model never rendered, at any camera distance/scale, with RasterizerState::CullNone, with lighting forced off, and even reduced to a single full-scale, identity-world, unlit, un-textured triangle drawn through the exact same Model/ModelMesh::Draw() path. A hand-built triangle at the same scale and PickingSample's own already-shipped, already-working Cylinder.model.json (loaded fresh into this sample's own code) both failed to render the same way, ruling out this mesh's own data, the camera, and render state as the cause. A large model (HeightmapCollision's sphere.model.json, 3252 vertices) rendered correctly through the identical code path at every scale tested (including scaled down to match the failing tests' size) — the key clue that the reader, not any one sample's own code or a specific asset, was responsible, and that it was somehow size/vertex-count sensitive.

Root cause, confirmed by direct source read plus a standalone sizeof() probe: every vertex struct in CNA (VertexPositionColor, VertexPositionTexture, VertexPositionColorTexture, VertexPositionNormalTexture) publicly inherits from the polymorphic Microsoft::Xna::Framework::Graphics::IVertexType (include/Microsoft/Xna/Framework/Graphics/IVertexType.hpp: virtual ~IVertexType() = default; plus a pure virtual getVertexDeclarationProperty()), so every instance carries an 8-byte vtable pointer that XNA's own "clean" (unpadded) vertex layouts never had. A standalone sizeof(T) probe (#includes only cna's own headers, no linking against any implementation needed since layout is fully determined by the declarations) confirms this live:

sizeof(VertexPositionColor)         = 40   (XNA/clean size: 16)
sizeof(VertexPositionTexture)       = 32   (XNA/clean size: 20)
sizeof(VertexPositionColorTexture)  = 56   (XNA/clean size: 24)
sizeof(VertexPositionNormalTexture) = 40   (XNA/clean size: 32)

ModelTypeReader::Read() (cna/src/Microsoft/Xna/Framework/Content/ContentManager.cpp, the "Meshes" parsing loop) selects which typed VertexBuffer::SetData overload to call for a mesh by comparing the .model.json-declared "vertexStride" (always one of the clean XNA sizes 16/20/24/32, since every conversion tool in this repo — tools/obj2model.py, tools/fbx_ascii2model.py — writes the tightly-packed layout, not the padded/vtable-inflated one) against sizeof(...) of these now-inflated structs:

if (stride == static_cast<int>(sizeof(Graphics::VertexPositionColor)))        // 40, never 16
    vb->SetData(reinterpret_cast<const Graphics::VertexPositionColor*>(vertBytes.data()), numVertices);
else if (stride == static_cast<int>(sizeof(Graphics::VertexPositionNormalTexture))) // 40, never 32
    vb->SetData(reinterpret_cast<const Graphics::VertexPositionNormalTexture*>(vertBytes.data()), numVertices);
else if (stride == static_cast<int>(sizeof(Graphics::VertexPositionColorTexture)))  // 56, never 24
    vb->SetData(reinterpret_cast<const Graphics::VertexPositionColorTexture*>(vertBytes.data()), numVertices);
else if (stride == static_cast<int>(sizeof(Graphics::VertexPositionTexture)))       // 32 !!
    vb->SetData(reinterpret_cast<const Graphics::VertexPositionTexture*>(vertBytes.data()), numVertices);

None of the declared "clean" stride values (16/20/24/32) equal their intended struct's real (vtable-inflated) size any more — except "vertexStride": 32, the value every Content.Load<Model>-based sample in this entire repo uses (since obj2model.py/ fbx_ascii2model.py only ever emit VertexPositionNormalTexture data), which accidentally equals sizeof(VertexPositionTexture) (also 32, by coincidence of the specific field-count/padding arithmetic — a real VertexPositionTexture is 8-byte-vtable + 12-byte Position + 8-byte TextureCoordinate = 28, padded up to 32 for 4-byte alignment). So the reader always silently dispatches to the wrong branch: it reinterpret_casts the raw, vtable-free file bytes (laid out as position[12] + normal[12] + texcoord[8], no vtable, exactly matching what the offline tools wrote) as an array of real, vtable-shifted VertexPositionTexture objects, and reads each vertex's .Position/.TextureCoordinate fields from the wrong byte offsets relative to the source buffer (a real VertexPositionTexture object has its vtable pointer at offset 0, Position at offset 8, TextureCoordinate at offset 20 — none of which match the file's actual offsets 0/12/24) — silently corrupting the position/texcoord data uploaded to the GPU for every single stride-32 .model.json in this entire repository, not just this one sample's asset.

Why this is very likely the true root cause of the long-tracked "near-plane-clipping" bug family (section 4/5 of NEXT.md, this item's own predecessor investigations across CameraShake/CustomModelClass/LensFlare/Graphics3D/PickingSample/TrianglePicking): a corrupted, effectively-arbitrary per-vertex byte-offset reinterpretation of a mesh's own position data would produce exactly the two symptoms already observed and attributed to "near-plane clipping" — a degenerate thin line (if the corrupted positions still land roughly in the same region of space, just scrambled) or full invisibility (if the corrupted positions land far outside the view frustum) — and does so without needing any actual clip-space/projection defect at all. No prior session ever directly inspected the EasyGL clipping code itself before attributing the symptom to it; this bug, by contrast, is confirmed by direct source read and a live sizeof() probe, and directly, reproducibly fixed here by bypassing the reader entirely (see below). Not re-attributed with 100% certainty this session — conclusively settling it for the other affected samples would mean re-testing tank.model.json/terrain.model.json/the spaceship's converted .obj through the same typed-SetData bypass used here, which is out of this task's own scope — but this is flagged as by far the most likely explanation, and a natural, well-scoped next step for whoever picks up section 8 task 2 (the "investigate near-plane clipping" task) next: read this item first, and try the bypass in one already-affected sample (CameraShake is the smallest) before assuming the bug is really in clip-space math.

Where to implement (if fixed in cna): cna/src/Microsoft/Xna/Framework/Content/ContentManager.cpp's ModelTypeReader::Read() needs to compare the declared "vertexStride" against the intended XNA-clean size of each vertex type (16/20/24/32 — i.e. hardcoded constants, or a sizeof computed over a memcpy-friendly bitwise-equivalent plain-data mirror struct with no IVertexType base), not sizeof() of the actual (vtable-carrying) Graphics::VertexPositionXxx types directly. Every other SetData call site that already constructs real, normal C++ vertex objects field-by-field (not via reinterpret_cast on a raw byte blob) — e.g. HeightmapCollision's Terrain.hpp, GeneratedGeometry's Terrain.hpp, this sample's own new CylinderModel.hpp — is entirely unaffected by this bug, since normal C++ member-access always resolves through the real (inflated) layout correctly; the bug is specific to ModelTypeReader::Read()'s stride-comparison-then-reinterpret_cast pattern.

Workaround used in InverseKinematics (#057): samples/InverseKinematics/src/ CylinderModel.hpp (NOXNA) reads cylinder_verts.bin/cylinder_idx.bin (produced once, offline, by the unchanged tools/obj2model.py) directly and constructs real, normally-initialized C++ VertexPositionNormalTexture objects (field-by-field, not a reinterpret_cast), then uploads them through the same typed VertexBuffer::SetData(const VertexPositionNormalTexture*, count) overload ModelTypeReader was trying (and failing) to reach. Confirmed live: renders correctly (a lit, visibly shaded, 20-link cylinder chain curling toward a target, exactly as expected) where the equivalent Content.Load<Model> path rendered nothing at any scale/distance tried. See samples/InverseKinematics/ missing.md for the full write-up including every isolation step performed (sphere vs. triangle vs. cylinder, scale sweeps, camera-distance sweeps, cull-mode sweeps, lighting on/off).

Superseded by the 2026-08-30 fresh audit: the paragraph above records the historical 2026-07 workaround only. CylinderModel.hpp and all raw/converted sidecars are now gone; the sample uses the byte-identical official XNA XNB and the shared CNA reader as described in the resolved-item update above and in the current samples/InverseKinematics/missing.md.

Blocked samples: every Content.Load<Model>-based sample in this repo is affected (their vertex data is silently corrupted), though most still render something recognizable enough that this wasn't caught before (a corrupted-but-similar-magnitude reinterpretation of a smoothly-varying, roughly-symmetric mesh like a sphere can still look "sphere-ish", just distorted) — this item doesn't retroactively mark any of them as newly broken, it re-attributes why the already-known thin-line/invisibility symptom happens. At the time, InverseKinematics (#057) worked around it completely via CylinderModel.hpp; the superseding 2026-08-30 note above records its removal.

Update 2026-07-10 (ChaseCamera, #058): independently confirmed a third and fourth time, on two more assets, at both size extremes. Per this task's own brief, the normal Content.Load<Model>("Ship")/Content.Load<Model>("Ground") path was tried and screenshot-tested before assuming this bug applied — confirmed live: a solid CornflowerBlue screen with only 2D HUD text visible, neither model rendered, no crash, across two screenshots 3 seconds apart, at this sample's own ~4031-unit initial camera distance (even farther than Graphics3D's ~3523-unit spaceship). Worked around with a new samples/ChaseCamera/src/RawModel.hpp (NOXNA, same bypass shape as CylinderModel.hpp/ Terrain.hpp, generalized to also bind a real Texture2D to the BasicEffect) — confirmed live this renders both models correctly, fully textured and shaded. This is the third and fourth .model.json asset independently confirmed to hit this exact bug: Ship_p1_wedge_geo1.model.json (32458 vertices, 16118 triangles — two orders of magnitude larger than InverseKinematics' 418-vertex cylinder) and Ground.model.json (6 vertices, 2 triangles — the smallest mesh yet tested through this bug), at both a much larger and a much smaller vertex count than the first confirmation, further reinforcing this is a structural reader defect uncorrelated with mesh size/complexity/source format (Ship.fbx converted via tools/fbx_ascii2model.py; Ground.x converted via assimp export + tools/obj2model.py). See samples/ChaseCamera/missing.md for the full account, including a separate (non-#26) per-asset triangle-winding quirk found in Ground.x's assimp export conversion, needing RasterizerState::CullNone for that one mesh.

Superseding update 2026-08-30 (ChaseCamera, #058): the historical paragraph above describes the removed loose-JSON conversion path, not the current sample. The fresh audit built the unchanged Windows Reach content and checked in its exact five XNB products. CNA now loads both stock ModelReader graphs, their BasicEffect shared resources and material textures through the original Content.Load<Model>("Ship")/("Ground") calls. Native OPENGLES3 agrees with XNA to 99.09% of pixels within 8 levels and 100% after a 4 px blur; real-Chrome WEBGL2 reaches 99.90% and 100%. RawModel.hpp, every JSON/PNG/raw-buffer sidecar and the per-ground CullNone toggle were deleted. The official ground renders with default state, proving the old winding issue came from the .x -> OBJ -> JSON conversion. See the current section at the top of samples/ChaseCamera/missing.md; the older account remains there only as labeled history.

Effort: S (the fix itself, once someone opens ModelTypeReader::Read(), is a few constants) — most of the actual work is verifying the fix against every already-shipped Model-based sample's screenshots to confirm no regression, since this reader is shared by all of them.


27. NetworkSession::SessionProperties has no mutable accessor and is never replicated over the wire — ✅ resolved 2026-09-01

Resolution (CNA c195fe8ce, SAMPLE-100): CNA now exposes the XNA-shaped mutable collection reference, sends the full nullable/signed property snapshot in the initial server welcome, detects host-side mutations during Update(), broadcasts reliable snapshots and applies them only from the authoritative transport host. Codec, getter, host-authority and real-ENet regression tests cover the original failure. The focused suite passed 29/29, the broad network suite passed 289/289, and a real two-process NetworkPrediction run proved all four host options arrive at the client without an application packet. The sample-local PacketKind/options-packet workaround was deleted. Current evidence: samples/NetworkPrediction/missing.md.

Historical finding while porting NetworkPrediction (2026-07-10). This sample's C# original (NetworkPredictionGame.cs's UpdateOptions()) relies on NetworkSession.SessionProperties being a live, mutable, host-authoritative, and automatically network-replicated key/value store: the host writes networkSession.SessionProperties[i] = value for 4 settings (simulated network quality, packet send rate, prediction on/off, smoothing on/off) once per frame, and every other machine in the session transparently sees the same values through its own networkSession.SessionProperties[i] read — real XNA's networking layer silently replicates this list to every peer with no explicit packet-send call anywhere in the sample's own code.

Root cause, confirmed by direct source read of cna's include/Microsoft/Xna/Framework/Net/NetworkSession.hpp and src/Microsoft/Xna/Framework/Net/NetworkSession.cpp:

  1. getSessionPropertiesProperty() returns const NetworkSessionProperties& — there is no non-const overload, no setSessionPropertiesProperty(), and no other public accessor that exposes a mutable reference to the session's own sessionProperties_ member. NetworkSessionProperties::operator[] does have a mutable, non-const overload (confirmed in NetworkSessionProperties.hpp) — but it is unreachable through NetworkSession's own const-only getter, so networkSession->getSessionPropertiesProperty()[0] = 5; does not compile (and could not be made to compile without a cna-side signature change).
  2. Even granting a hypothetical mutable accessor, grepping NetworkSession.cpp's Update() and every other member function shows sessionProperties_ is set exactly once, at construction (NetworkSession(NetworkSessionProperties properties, ...)'s member-initializer list), and never read or written again anywhere in the class outside the getter above. There is no wire-replication logic of any kind for this member — confirmed by grepping for sessionProperties_/SessionProperties across the whole .cpp file (only the constructor and the one getter reference it).

So this gap is really two independent pieces: (a) no way to mutate the properties list after Create()/Join() even locally, and (b) no automatic replication mechanism exists even if (a) were fixed. Both would need to be implemented in cna to port this sample's SessionProperties usage faithfully.

Historical CNA port behaviour (workaround applied in NetworkPrediction, #100): the host still computes the same 4 settings from its own local key input (UpdateOptions(), unchanged in shape from the original), but instead of writing them into NetworkSession.SessionProperties, it explicitly broadcasts them in a small, NOXNA "options packet" — a leading PacketKind byte (TankState = 0/Options = 1) distinguishes it from an ordinary tank-state packet on the same LocalNetworkGamer::SendData/ReceiveData channel, sent by the host (via (LocalNetworkGamer*)networkSession_->getHostProperty()) at the same throttled cadence as tank packets (sendPacketThisFrame). Client machines apply the received values directly to their own local networkQuality_/framesBetweenPackets_/ enablePrediction_/enableSmoothing_ fields in ReadIncomingPackets(), instead of reading them back from SessionProperties as the C# original does. See samples/NetworkPrediction/src/NetworkPredictionGame.hpp's top-of-file comment and samples/NetworkPrediction/missing.md for the complete account. Confirmed live: a solo/single-gamer session created via NetworkSession::Create() renders the correct, live DrawOptions() text ("Packets per second = 10 (B to change)", etc.) driven by these locally-tracked fields — the substitute mechanism reproduces the original's visible behavior for the one-machine case that could be tested this session; the actual host → client replication path itself was not tested with a second live instance (same "no genuine 2-machine LAN test" caveat every networking sample in this repo carries — see DEFERRED.md items #19–21's own verification notes and NEXT.md section 5).

Historical implementation direction: NetworkSession needed (a) a mutable accessor for sessionProperties_ (either a non-const getSessionPropertiesProperty() overload, matching the pattern NetworkSessionProperties::operator[] itself already uses, or a dedicated setter), and (b) actual replication logic in Update()/the ENet backend — e.g. treating a change to sessionProperties_ like GamerJoined/GamerLeave, diffing and broadcasting it as its own wire message whenever the host's copy changes, and applying an incoming one on non-host machines. This is squarely a "generic real-time-networking feature," not Xbox-Live-specific.

Update (2026-07-10, PeerToPeer #103 ported): checked, as this item itself recommended — PeerToPeerGame.cs (PeerToPeer's C# original) never references NetworkSession.SessionProperties anywhere. This gap is confirmed genuinely conditional on a sample's own use of SessionProperties, not universal to every NetworkSession-based sample: PeerToPeer needed no workaround for it and no PacketKind-byte packet-type disambiguation anywhere in its own Tank.hpp (every packet on the wire is always the same shape). See samples/PeerToPeer/missing.md for the full account.

Current blocked samples: none for this framework issue. NetworkPrediction's native port now uses the exact property contract. Its remaining SAMPLES-DEC-006 state is the independent Emscripten discovery/inbound-host boundary. PeerToPeer does not use SessionProperties.

Historical effort estimate: S/M. The completed change included both the public mutation path and reliable host-to-client replication; it was not closed by the getter alone.


28. EasyGL: a full-backbuffer SpriteBatch draw before any 3D draw call in the same frame breaks that frame's 3D rendering entirely — investigated, not reproduced (2026-07-11 confirmed)

Update (2026-07-11): cna's own Task 933 (landed 2026-07-10) investigated this directly and could not reproduce it across 4 targeted repro attempts. Status stays open/unresolved (not closed as fixed, not closed as invalid) — ReachGraphicsDemo's existing DrawBackgroundQuad() workaround should remain in place. Task 933 did find a separate, real, latent bug while investigating (EasyGLSpriteBatchBackend::FlushBatch() never restores the GL viewport after resetting it) but did not establish a link between that and this item's original symptom — treat as two separate findings, not one resolved by the other.

Found while porting ReachGraphicsDemo's EnvmapDemo (2026-07-10). A SpriteBatch.Begin()/Draw()/End() call that stretches a texture to cover the entire backbuffer (GetSpriteBatch().Draw(*background_, Rectangle(0, 0, 480, 800), ...), matching EnvmapDemo.cs's own SpriteBatch.Draw(background, new Rectangle(0, 0, 480, 800), Color.White); exactly), executed before any 3D GraphicsDevice:: DrawIndexedPrimitives/DrawUserIndexedPrimitives call in that same frame, makes every subsequent 3D draw call in that frame render nothing at all — confirmed live via screenshot isolation, one variable at a time:

  • Removing just this one SpriteBatch.Draw() call (nothing else changed) made the scene's own 3D model (a saucer, loaded via the same RawMesh-style bypass already proven working elsewhere in this repo — see item #26) render correctly.
  • Re-adding it after swapping the 3D effect (EnvironmentMapEffect → plain BasicEffect), after switching to a much closer/simpler camera (CreateLookAt((0,500,2000), (0,0,0), Up) instead of the original's own (4500,-400,0)-distance camera), and after forcing RasterizerState::CullNone — each tested independently — made no difference; the 3D content stayed invisible in all three cases as long as the full-backbuffer SpriteBatch draw remained in the frame.
  • Converting the source image from JPEG to an ordinary RGBA PNG also made no difference, ruling out the source texture's own decoded format (e.g. missing alpha channel) as the cause too.
  • The same sample's own DrawTitle() calls (a much smaller SpriteBatch draw of text only, also positioned before the 3D content in every one of this sample's 5 ported demo scenes) do not trigger the bug — every other demo in this sample draws DrawTitle() immediately before its own 3D content and renders correctly. This narrows the trigger specifically to "a SpriteBatch draw covering the entire backbuffer," not merely "any SpriteBatch call before a 3D draw in the same frame."

This sample's own explicit BlendState/RasterizerState/DepthStencilState/ SamplerState resets immediately before the 3D draw call do not clear whatever state is left dirty — whatever the actual GL-level cause is (a stale bound shader program, a leftover vertex array/attribute binding, a scissor/viewport side effect, etc.), it survives all four of those state resets.

Where to implement (if fixed in cna): almost certainly cna/src/CNA/Internal/Backends/EasyGL/EasyGLGraphicsBackend.cpp's SpriteBatch flush/draw path (whichever internal method actually issues the batched full-screen quad's glDrawElements/glDrawArrays call) — not yet directly inspected to find the exact GL state it leaves dirty afterward, since isolating and fixing this was out of scope for this porting session (no cna edits per this repo's own porting-session constraints).

Workaround used in ReachGraphicsDemo (EnvmapDemo, #005): the background image is drawn as a manually-built full-screen quad via BasicEffect + DrawUserIndexedPrimitives (EnvmapDemo::DrawBackgroundQuad(), NOXNA — World/View/ Projection all Matrix::Identity, so the quad's own vertex positions are already in clip space) instead of SpriteBatch, entirely avoiding the problematic call shape. Confirmed live this fully resolves the issue: background image, title text, and the 3D saucer (fully textured, lit, and showing a genuine reflective cubemap look) all render correctly together in the same frame. See samples/ReachGraphicsDemo/missing.md for the complete isolation account.

Blocked samples: ReachGraphicsDemo (#005, EnvmapDemo specifically — worked around). No other currently-ported sample in this repo draws a full-backbuffer SpriteBatch sprite before any 3D content in the same frame, so this is not currently known to affect anything else, but any future sample doing the same (a full-screen 2D background image behind a 3D scene) should watch for this.

Effort: M — needs someone to actually step through EasyGLGraphicsBackend's SpriteBatch draw path and its 3D draw path back-to-back with a GL state debugger/ glGetError-style instrumentation to find exactly what state differs; the fix itself is likely small (an explicit state reset) once found.


29. EasyGL: DualTextureEffect's own shader hardcodes a Position+UV-only (no Normal) vertex attribute layout

Found while porting ReachGraphicsDemo's DualDemo (2026-07-10). CNA's EnsureDualTextured3DProgram() (EasyGLGraphicsBackend.cpp) declares:

layout(location=0) in vec3 aPos;
layout(location=1) in vec2 aUV;

— a 2-attribute layout with no Normal in between — unlike BasicEffect's/ EnvironmentMapEffect's own lit shaders (EnsureLit3DProgram()/ EnsureEnvMapped3DProgram()), which both expect the 3-attribute Position+Normal+UV layout (VertexPositionNormalTexture's own real declaration: Position at location 0, Normal at location 1, TextureCoordinate at location 2). This is undocumented and differs from the sibling lit-effect shaders in the same file, easy to miss.

Uploading this repo's usual VertexPositionNormalTexture (built via the same RawMesh.hpp-style bypass used for every other model in this repo — see item #26) against DualTextureEffect makes the shader's aUV (declared at location 1) silently read from whatever is actually bound at that location — the mesh's own Normal.xy — instead of the real per-vertex UV. Confirmed live via screenshot: every submesh rendered as a flat, uniform color (each face's own constant per-face normal producing a constant "fake UV," landing on one arbitrary texel), not the real tiled texture/ lightmap detail; the model's silhouette/shape was otherwise completely correct, ruling out a geometry problem.

Where to implement (if fixed in cna): either (a) document this required vertex layout clearly on DualTextureEffect itself (a comment/doc gap, not a behavior gap — arguably this is by design, matching real XNA's own VertexPositionDualTexture-style expectation, in which case the fix is purely documentation), or (b) if genuine uniformity across effect types is preferred, extend EnsureDualTextured3DProgram() to also accept a 3-attribute Position+Normal+UV layout (ignoring Normal) so callers don't need a separate vertex-upload path just for this one effect type.

Workaround used in ReachGraphicsDemo (DualDemo, #005): a new RawMeshPosTex.hpp (NOXNA, alongside RawMesh.hpp) uploads plain VertexPositionTexture (Position + UV only, discarding the Normal field from the same already-converted sidecar files) for this one demo's meshes specifically. Confirmed live this fully resolves the issue — see samples/ReachGraphicsDemo/missing.md for the complete account, including a second, independent bug found and fixed in the same investigation (tools/fbx_ascii2model.py silently used the wrong one of 2 UV layers on model.fbx's meshes).

Blocked samples: ReachGraphicsDemo (#005, DualDemo specifically — worked around). No other currently-ported sample in this repo uses DualTextureEffect with a VertexPositionNormalTexture-shaped bypass mesh.

Effort: S (documentation) or M (shader/attribute-layout generalization, if pursued).


30. tools/fbx_ascii2model.py assumed LayerElementNormal is always "ByPolygonVertex" — it usually isn't

Found while porting RimLighting (2026-07-10). head.fbx's own LayerElementNormal block declares MappingInformationType: "ByVertice" — exactly one normal per unique vertex/control-point (same array length as Vertices:, confirmed live: both exactly 8213 entries for this mesh), indexed the same way Vertices: is (pos_idx), not per-polygon-corner the way UVs are. tools/fbx_ascii2model.py's build_buffers() unconditionally indexed the Normals: array by flat_idx (the per-polygon-corner flat index) regardless of this field — only correct for "ByPolygonVertex"-mapped files. For a "ByVertice"-mapped mesh with more polygon corners than unique vertices (the overwhelmingly common case for any mesh with vertex sharing), this silently reads normals from unrelated array slots for the corners where flat_idx happens to stay in bounds, and falls back to a hardcoded "straight up" (0,1,0) default for every other corner (~75% of them, for head.fbx).

Confirmed live via isolation: a first conversion pass produced a recognizable head silhouette with severe, localized jagged/scrambled dark patches wherever the environment-map reflection was active. A temporary debug build forcing EnvironmentMapAmount to 0 (isolating out the reflection term, since DirectionalLight0's own DiffuseColor is never set anywhere in Game1.cs, defaulting to black) rendered a perfectly clean, artifact-free silhouette — proving the raw geometry/positions were fine and narrowing the defect specifically to normal-dependent shading. Direct source read of head.fbx plus a length comparison (len(normals) == len(positions) == 8213, vs. len(poly_indices) == 32752 corners) confirmed the root cause above.

A repo-wide grep across every FBX file used by this session's already-shipped samples found "ByVertice" is actually the more common of the two mapping modes: Ship.fbx (ChaseCamera, converted directly via tools/fbx_ascii2model.py) and saucer.fbx/model.fbx (ReachGraphicsDemo, same tool) are all "ByVertice" too — meaning this bug likely also silently corrupted their own already-shipped _verts.bin normals (not re-verified or re-shipped as part of this task — out of scope for a single-sample porting session). Only tank.fbx (ReachGraphicsDemo, converted by its own separate one-off script, not this shared tool), P2Wedge.FBX/Cats.FBX (PickingSample/TrianglePicking), and maze1.FBX/marble.FBX (MarbleMaze, binary FBX, different pipeline) are confirmed "ByPolygonVertex" or not applicable — those are not suspected of being affected.

Fix (applied 2026-07-10): parse_mesh_block() now also returns the LayerElementNormal's own MappingInformationType; build_buffers() takes it as a new parameter (default "ByPolygonVertex", preserving old behavior when absent) and indexes Normals: by pos_idx when the mode is "ByVertice"/"ByControlPoint", by flat_idx otherwise. Confirmed this does not change any already-shipped "ByPolygonVertex" asset: re-ran P2Wedge.FBX and Cats.FBX (PickingSample) through the fixed tool and diff'd the output _verts.bin/_idx.bin byte-identical to the already-shipped files.

What's needed for full remediation (not done this session): re-run tools/fbx_ascii2model.py against Ship.fbx (ChaseCamera) and saucer.fbx/model.fbx (ReachGraphicsDemo), diff the resulting _verts.bin against the currently-shipped ones (a diff would confirm real impact), and if they differ, re-screenshot each affected sample to check whether the corrected normals produce a visibly different (and more correct) render — most likely to matter for ReachGraphicsDemo's EnvmapDemo (saucer, rendered with reflective EnvironmentMapEffect, the same effect class that made this bug so visually obvious for RimLighting) and less likely to be visually obvious for ChaseCamera's Ship (rendered with diffuse-textured BasicEffect, where wrong normals mostly just look like slightly-off shading rather than scrambled reflections).

Blocked samples: RimLighting (#037 — fixed via the tool fix, now ships correct normals). ChaseCamera (#058) and ReachGraphicsDemo (#005) are suspected to have subtly-wrong (not necessarily visibly broken) normals in their own already-shipped Ship/saucer/model assets — not confirmed broken, not re-shipped.

Effort: S (the fix itself is done); re-verifying/re-shipping the 2 other suspected samples is effort S each (re-run the tool, diff, screenshot-compare).


Summary Table

# Feature Repo Effort Samples blocked
1 XNB → open format pipeline all M/sample all
2 SpriteFont loading cna L many
3 EasyGL: DiffuseColor ignored for VertexPositionColor cna S Primitives3D
4 EasyGL: Wireframe mode (OpenGL ES has no glPolygonMode) cna M Primitives3D
5 VertexPositionNormal + lit shader cna L LensFlare, Graphics3D, PickingSample, TrianglePicking, HeightmapCollision, CustomModelClass, InverseKinematics, ChaseCamera, MarbleMaze
6 Model asset conversion, static geometry (.x/.fbx → .model.json) tools M/model many
7 Audio playback cna
8 SpriteBatch.DrawString / SpriteFont cna M most
9 Viewport.AspectRatio cna 0
10 GamePadButtons direct access cna 0 (workaround)
11 Shader conversion (HLSL .fx → GLSL .shader.json) tools M/shader many Phase 3+
12 RenderTarget2D cna
13 Skeletal animation playback samples + CNA XNB none by blanket inference
14 TextureCube content loading (Content.Load<TextureCube>) cna S none blocking (RimLighting/EnvmapDemo both bypassed via direct TextureCube::SetData())
15 Accelerometer/sensor platform reality cna + samples S AccelerometerSample ✅ freshly re-ported 2026-09-01 with both original branches and CNA browser classification fix 35268971c; TiltPerspective ✅ historical owner-approved input deviation; Orientation (miscategorized, likely portable); Geolocation (still genuinely blocked)
16 Microphone capture cna M MicrophoneEcho
17 Multiplayer networking (NetworkSession-alike) cna L/XL ClientServerSample, NetworkPrediction, PeerToPeer (NetRumble still needs item 11)
18 Content-pipeline processor extensibility tools L none
19 GamerServicesDispatcher::Update() no-op hangs NetworkSession::Create/Find/Join cna M ClientServerSample, NetworkPrediction, PeerToPeer, NetRumble
20 NetworkGamer.IsHost/Id hardcoded stubs cna M ClientServerSample, NetworkPrediction, PeerToPeer, NetRumble
21 Initial GamerJoined event queued, not synchronous cna + sharp-runtime S ClientServerSample, NetworkPrediction, PeerToPeer
22 EasyGL: BlendState.ColorWriteChannels ignored (no glColorMask) cna S LensFlare
23 Game::DoInitialize() wires ComponentAdded after calling Initialize() cna S Graphics3D, PickingSample (workaround applied, still valid/needed); TrianglePicking confirmed NOT affected (constructor-time add)
24 GraphicsDevice::Clear(Color) never clears depth buffer cna S all 3D samples (latent, not blocking)
25 VertexBuffer/IndexBuffer have no GetData() (no GPU buffer readback) cna S/M none outright (tool-level workaround used by TrianglePicking)
26 ModelTypeReader vertex-stride/IVertexType-vtable size mismatch corrupts all stride-32 .model.json vertex data (likely true cause of the "near-plane-clipping" bug family) cna S old loose-JSON ports only (InverseKinematics and ChaseCamera bypasses were removed in favor of their exact official XNB paths on 2026-08-30; MarbleMaze and ReachGraphicsDemo still require their own fresh audits)
27 NetworkSession.SessionProperties has no mutable accessor and is never replicated over the wire cna S/M NetworkPrediction now uses the exact property contract; PeerToPeer does not use SessionProperties
28 EasyGL: a full-backbuffer SpriteBatch draw before any 3D draw in the same frame breaks that frame's 3D rendering cna M ReachGraphicsDemo (EnvmapDemo — worked around via a hand-built 3D quad instead of SpriteBatch)
29 EasyGL: DualTextureEffect's shader hardcodes a Position+UV-only (no Normal) vertex attribute layout, unlike BasicEffect/EnvironmentMapEffect cna S/M ReachGraphicsDemo (DualDemo — worked around via a Position+UV-only vertex upload, RawMeshPosTex.hpp)
30 tools/fbx_ascii2model.py assumed LayerElementNormal is always "ByPolygonVertex" (usually "ByVertice") tools S RimLighting ✅ fixed; ChaseCamera no longer uses the converted Ship asset (exact official XNB verified 2026-08-30); ReachGraphicsDemo's saucer/model assets still need fresh audit