Historical gap ledger (2026-08-22):
plan.mdis now authoritative. Every cited blocker must be re-tested against../cnanextand../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 inplan.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
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.BuilderormgcbCLI 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
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).
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
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
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)
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 viaContent.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
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_PHONEsplit, no keyboard/gamepad branch anywhere inGame.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-#ifan 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-135found the original's ownGetState()does have a keyboard fallback, gated onMicrosoft.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 aVector3(Left/Right→X--/X++,Up/Down→Y++/Y--,Zfixed at-1, thenVector3.Normalize()'d). This fallback branch lives inside the same#if WINDOWS_PHONEblock as the real-hardware branch (not in a separate non-#ifbranch the way Yacht/ SnowShovel/Bounce's own fallbacks do), which is why the earlier audit's "no#if WINDOWS_PHONEsplit" 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 runtimeDeviceTypecheck nested inside the#if, rather than the#ifitself). Seesamples/AccelerometerSample/missing.mdfor the full account and the exact ported code. Not yet re-audited for TiltPerspective — a future session porting that sample should re-checkAccelerometerHelper.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 reportedDeviceType::Devicein a browser even though its Web target has no sensor backend. CNA35268971cnow reportsEmulatoronly for Web and preservesDeviceon 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.cswas re-checked against the exact same "look for a nestedDeviceTypebranch inside#if WINDOWS_PHONE" correction found while porting AccelerometerSample, rather than assumed identical either way. Result:AccelerometerHelper.cshas no#if WINDOWS_PHONEsplit at all (confirmed by the.csproj's singleWindows Phonebuild 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/Right→X∓/X±,Up/Down→Y±/Y∓,Zfixed 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. Seesamples/TiltPerspective/missing.mdfor the full account, including a second, separate finding (confirmed via the real FNA source, not assumed): the sample's ownGeometricPrimitive.csnever setsDirectionalLight0.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 acna-side fix or a new user product-scope decision).
- 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
- 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 ofLayoutSample.cs/OrientationSample.cs: zero references toAccelerometer/Compass/any sensor class anywhere. CNA already hasDisplayOrientation.hpp, andGraphicsDeviceManager/GameWindowboth implementSupportedOrientations/CurrentOrientation(this is the same subsystem behind the already-fixed portrait-orientation bug — see the project's ownfeedback_cna_portrait_orientation_bugmemory). 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.
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/.fbxfiles to.model.json+ binary buffers. Tools:assimp export+tools/obj2model.py, ortools/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
ModelMeshparent-bone setter plus aModelTypeReader::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.json — not 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).
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.
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").
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)
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: —
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
.fxshader logic to GLSL (vertex + fragment) - Create
.shader.jsondescriptor per effect - Tools: manual port or
SPIRV-Cross+dxcpipeline
Blocked samples: BloomSample (3 shaders), DistortionSample, NonPhotoRealistic, NormalMapping, PerPixelLighting, VertexLighting, RimLighting, ShadowMapping, ShatterEffect, Particles3D, and all Phase 3+ shader samples.
Effort: XL
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.
Corrected 2026-08-30 by SAMPLE-051 and SAMPLE-054. This item mixed two different things:
- an optional CNA-specific open
.model.jsonskeletal schema and conversion tool; and - the behavior the original XNA samples actually use: official pipeline XNB models carrying
skinned vertex channels,
SkinnedEffect, and sample-owned animation data inModel.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.
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)
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.
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.
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'sIsHostis true" — since that's unconditionally true,NetworkSession.IsHostis also always true, on every machine, host or client.NetworkSession::FindGamerById(id)does a lineargetIdProperty() == idscan — since every gamer'sIdis 0, this always returns the first gamer inAllGamers, regardless of whichidwas actually requested. Any protocol that writesgamer.Idinto 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.
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.
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:
getSessionPropertiesProperty()returnsconst NetworkSessionProperties&— there is no non-const overload, nosetSessionPropertiesProperty(), and no other public accessor that exposes a mutable reference to the session's ownsessionProperties_member.NetworkSessionProperties::operator[]does have a mutable, non-const overload (confirmed inNetworkSessionProperties.hpp) — but it is unreachable throughNetworkSession's own const-only getter, sonetworkSession->getSessionPropertiesProperty()[0] = 5;does not compile (and could not be made to compile without acna-side signature change).- Even granting a hypothetical mutable accessor, grepping
NetworkSession.cpp'sUpdate()and every other member function showssessionProperties_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 forsessionProperties_/SessionPropertiesacross the whole.cppfile (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 sameRawMesh-style bypass already proven working elsewhere in this repo — see item #26) render correctly. - Re-adding it after swapping the 3D effect (
EnvironmentMapEffect→ plainBasicEffect), 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 forcingRasterizerState::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 drawsDrawTitle()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).
| # | 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 |