EasyGL state, clears, targets, queries and buffers: current semantics
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Read in EasyGLRenderer.cpp, its header, GlProfile.hpp and the shared GL headers at 009d40f5; the named EasyGL tests exist and were not executed, and no browser (WebGL) behaviour was observed.
This page records what the EasyGL family — the five GL-profile identities — actually does at snapshot 009d40f5 for the operations where GL and XNA disagree by default: wireframe, occlusion counts, colour write masks, clears, stencil faces, fog, base-vertex draws, render-target usage and mips, volume and cube storage, the default back buffer, context-loss policy, viewport and scissor. Many older statements about this family, including items in CNA's own bug list, are stale in one direction or the other; each section says what current code does and, where the history is instructive, what used to be wrong. It is for anyone fixing or testing a GL-profile bug and for anyone comparing an EasyGL result with another renderer.
CNA's own EasyGL bug list is dated
docs/easygl_bugs.md, CNA's per-bug record for this renderer, carries its own staleness banner: it was last fully written before thousands of lines of later EasyGL changes, only a few rows were independently re-confirmed, and the rest were "spot-checked, not exhaustively re-verified". docs/rendertarget-support.md likewise still lists render-target gaps that later code closed. Treat both as a record of methodology, not as a current defect list; the source is the authority, and every section below states what it does now.
Closed since the original bug list
- Anisotropic filtering reaches the GPU: the requested
MaxAnisotropyis converted as XNA does (to an unsigned value), clamped to the context'sGL_MAX_TEXTURE_MAX_ANISOTROPYand applied only whenGL_EXT_texture_filter_anisotropicis advertised — through sampler objects, or as a texture parameter on the ES 2.0 generation, which has none.SupportsCapability(AnisotropicFiltering)answers the same extension test. - Cube render targets take a real
depthFormatparameter instead of an "always has depth" flag, and each face of a multisampled cube owns its own multisample colour renderbuffer. - Multiple render targets use one reusable MRT framebuffer.
FinalizeCurrentMRT()runs before every destination change and resolves and regenerates the surviving members, andSetRenderTargetsinstalls the new ordered set only after refusing an over-long set, foreign or mismatched targets, a repeated subresource, unequal per-slot colour masks on a profile without indexed masks, and pending GL errors. The public fixtureEasyGL_MRT_TwoAttachments(easygl_mrt_test.cpp) now writes distinct outputs to one through four attachments and covers set replacement, per-target colour masks, MSAA resolve, usage, readback and immediate sampling. Its predecessor is a lesson in oracle design: it expected an old blue value in attachment 1 to survive aDiscardContentsbind drawn through single-outputBasicEffect, so it could never tell "attachment 1 is inactive" from "the shader writes only attachment 0". - Stock fog and Fresnel are computed the way FNA and XNA define them (fog from FNA's CPU-prepared view-space fog vector, Fresnel per vertex as in XNA's
EnvironmentMapEffect), by source reading; the two oracle scenes that exposed the old defects were not re-measured (see below). - SpriteBatch index wrap is gone (see the historical case).
Base-vertex draws on every profile
glDrawElementsBaseVertex is core only from GLES 3.2 and desktop 3.2, so a GLES 3.0 or WebGL context may not have it, and meta-gl terminates the process when a missing entry point is called. At this snapshot only OPENGL33 uses the native call; every other profile, OPENGLES3 included, satisfies a nonzero base vertex by shifting each enabled per-vertex attribute pointer by the base and restoring it after the draw (RequiresBaseVertexPointerRebase in GlProfile.hpp; DrawIndexedWithBaseVertexFallback). A negative base vertex is folded into a rewritten index slice. An earlier state in which the ES 3.0 path reached the native call unchecked — a deterministic process-failure risk on a strict ES 3.0 driver — is not present.
Occlusion queries: exact where GL can count
XNA's OcclusionQuery::PixelCount() is a real fragment count. EasyGL asks for GL_SAMPLES_PASSED on the first query of the process and keeps it if the driver accepts it, which desktop GL does; otherwise it uses the ES 3.0 and WebGL 2 core target GL_ANY_SAMPLES_PASSED, whose answer is only 0 or 1. OcclusionQuery::isPixelCountPreciseEXT() reports which one is in effect. A game that divides PixelCount() by an area to get a coverage ratio — the lens-flare idiom — gets 1/area on the boolean target, so it should ask first. The ES 2.0 generation has no occlusion queries.
Wireframe is native or refused
FillMode.WireFrame uses a real polygon mode: glPolygonMode on desktop contexts, GL_NV_polygon_mode on ES where it is exposed, and the WEBGL_polygon_mode extension in a browser. SupportsCapability(WireFrame) is true exactly when one of these exists. Without one, a wireframe triangle draw is refused with an explanation instead of being approximated by re-expanding triangles to GL_LINES: line expansion cannot keep culling, clipping, polygon depth bias, MSAA and stencil behaving as they do for filled triangles. Older descriptions of a line-expansion implementation that needed no polygon mode describe a replaced design.
Fog and Fresnel in the shared stock shaders
Stock-effect fog is a view-space term computed from a fog vector the CPU prepares exactly as FNA's EffectHelpers.SetFogVector does, baking the third column of World × View together with FogStart and FogEnd. The vertex shader keeps vFogFactor = 1 - clamp(dot(position, uFogVector), 0, 1) and the fragment shader mixes towards the fog colour; a zero vector disables fog and the degenerate FogStart == FogEnd case yields full fog, both decided on the CPU. The environment-map Fresnel scalar is evaluated per vertex from the vertex normal and eye vector before interpolation, as XNA's EnvironmentMapEffect does. These live in the corpus shared with OPENGL4 (GlStockShaderSources.hpp), so they apply to every stock variant on six identities. A closed-form fog formula written in terms of FogStart and FogEnd directly, as some older descriptions quote it, is not what the shader computes. easygl_viewspace_fog_test.cpp and the per-effect fog tests exercise the path.
Colour write masks
ApplyBlendState stores all four ColorWriteChannels values. ApplyCurrentColorWriteMasks() submits slot 0 through glColorMask and, on a context with indexed colour masks (a non-WebGL ES 3.2 or desktop 3.0 context with glColorMaski loaded), each MRT slot independently; a profile without indexed masks refuses an MRT set whose active slots need different masks rather than silently collapsing them. GL clears respect the colour mask while XNA clears do not, so each EasyGL clear temporarily enables every channel and then restores the requested masks.
Clears
The seven renderer clear entry points issue immediate glClear calls, and the depth and stencil variants force full depth and stencil write masks for the clear. The colour-only entry point, to which the device routes a bare ClearOptions::Target and which the raw-float Clear(r, g, b, a) overload calls directly, clears colour only; it used to add the depth bit, so a colour-only request could clear depth, or leave it alone merely because an earlier state had disabled depth writes. GL ignores the viewport for a clear but honours an enabled scissor, while an XNA clear covers the complete target; every EasyGL clear therefore disables the scissor test for the clear and restores it. Aspect selection itself — which aspects exist and when an explicit depth or stencil request throws — happens in the device before EasyGL is reached; see Targets, clears and viewports. The overload matrix is exercised by easygl_clear_overloads_test.cpp, which checks resulting colours (and that a depth-only clear leaves colour alone) but never primes depth first. The colour-only fix above is pinned instead by leg C5 ("colour-only clear keeps depth") of backbuffer_pass_order_test.cpp: it writes depth, issues a ClearOptions::Target clear in a later cycle and requires a farther quad to be rejected by the earlier depth. That shared file is registered for EasyGL as EasyGL_Backbuffer_PassOrder, and for OPENGL4, SDL_GPU, Vulkan and WebGPU as well.
Render targets: usage, mips and formats
- Usage.
CreateRenderTarget2DandCreateRenderTargetCubedeliberately ignore theirpreserveContentsargument. A framebuffer bind never touches its attachments, so every target preserves by construction, and the shared device clears an exactDiscardContentstarget to black on each real bind.PreserveContentsandPlatformContentstherefore behave identically, and discard is a shared clear, not a renderer load action. - Mips. A render target created with a mip chain regenerates it from level 0 when it is unbound, and a cube target per face. An ordinary
Texture2Dnever regenerates: explicitSetDatalevels are stored as uploaded, and declared but unwritten levels are allocated without invented content. - Cube and volume formats. Cube textures honour the requested
SurfaceFormatthroughClassifyTextureCubeFormatEXT—Color, the DXT formats andAlpha8everywhere, packed 16-bit, 10-bit and float formats outside the ES 2.0 generation, the 16-bit normalised formats where the context hasEXT_texture_norm16— and volume textures throughClassifyTexture3DFormatEXT; an unsupported format is refused, not stored as RGBA8. - Context-loss recovery. 2D textures and cube textures join the family's recovery registry;
EasyGLTexture3DRendereris not a recoverable resource and is created without the registry, so a volume texture's content is not restored after a GL context loss. That matters where loss really happens (WebGL 2) and is recorded for Known Issues review.
Effect unbinding
EasyGLEffectRenderer::Unbind() is intentionally empty: there is no glUseProgram(0), because the next bind or SpriteBatch flush installs its own program anyway. Code that relies on "no program bound" after End will not observe it on this family.
Historical worked case: the 16-bit sprite index ceiling
EasyGL's SpriteBatch builds four vertices and six 16-bit indices per sprite. An earlier implementation accumulated every same-texture sprite until End() and narrowed the vertex count to uint16_t for the index base, so the threshold was computable in advance:
constexpr int kMaxUint16Value = 65535; // largest 16-bit index
constexpr int kVerticesPerQuad = 4; // one sprite = 4 vertices
constexpr int kMaxQuadsBeforeWrap = (kMaxUint16Value + 1) / kVerticesPerQuad; // = 16384
The 16,385th sprite in one batch wrapped its base index from 65,536 to zero and silently redrew the first sprite — a failure a particle system, a tile map drawn one sprite per tile, or glyph-by-glyph text could reach without any error. Current code chunks at XNA's own submission size instead: kMaxSpritesPerBatch is 2,048, and a batch that reaches kMaxSpritesPerBatch * 4 vertices is flushed and continued, so the queue is unbounded from the caller's point of view as it is in XNA. The case remains instructive for any renderer that narrows an index type: compute the ceiling, then test exactly at and one past it rather than assuming "large enough".
The default back buffer
The EasyGL factory receives the requested depth format, and UpdatePresentationFormatEXT accepts a later change of it through GraphicsDevice::Reset; the multisample back-buffer framebuffer rebuilds its colour and depth(-stencil) renderbuffers for the selected DepthFormat. The colour format is RGBA8 whatever was requested, and the family reports SurfaceFormat::Color as the applied back-buffer format. Without multisampling the back buffer is the platform's own framebuffer 0, and its depth and stencil planes are fixed when the GL context is created: the family's context request (RequestedGlContext) asks for 24 depth bits, 8 stencil bits and double buffering whatever DepthFormat the game selected, so requesting None or Depth16 does not shrink or remove it. The requested format decides only which tests the renderer enables (the depth test needs a format other than None; the stencil test needs Depth24Stencil8, although the context request always asks for stencil bits) and how the renderbuffers of the multisample framebuffer are built when back-buffer MSAA replaces framebuffer 0. A render target's stencil test, by contrast, follows the plane that target really owns. easygl_depth_format_test.cpp (EasyGL_DepthFormat) cycles all four DepthFormat values through both the manager and the direct SetPresentationParameters route and checks storage and survival; the shared depth-stencil behaviour tests check what the planes accept.
Context-recovery policy and the debug keys
With context recovery disabled — GraphicsDevice::SetContextRecoveryEnabled(false) after device construction and before loading content — two things change: shared Texture2D code discards CPU pixels after a full upload, and EasyGL stops registering newly created GL resources with its recovery registry (RegistryPtr() returns null). Existing resources are not unregistered and re-enabling reconstructs nothing, so the toggle is not a reversible migration. On desktop, where a GL context is not taken away, this is a memory saving; in a browser it gives up recovery.
The F9 and F10 debug keys rehearse loss on this family. On desktop, DebugSimulateContextLoss performs a complete synchronous loss-and-restore cycle (notify loss, recreate the platform context, reload the loader and dispatch tables, notify restore), and DebugRestoreContext runs the same full cycle, so either key is a whole rehearsal. In a browser F9 asks WebGL to lose the context and F10 asks it to restore it, and both complete asynchronously through the browser's context-lost and context-restored events; CanBeginDrawEXT() keeps Draw from running in between. The resource side of loss and restore is on EasyGL internals: context loss.
Viewport and scissor
SetViewport() programs the GL viewport and depth range immediately, for the back buffer, 2D targets and cube faces, flipping Y against the active target's height. EasyGL's SpriteBatch keeps a genuine custom sub-viewport and builds its orthographic projection from that viewport's logical size, as XNA does; it compares against the presentation rectangle rather than the raw GL viewport, so a letterboxed default viewport is not mistaken for a game-set one. The scissor rectangle is set by SetScissorRect (Y-flipped against the active target height) and enabled by ApplyRasterizerState. The applying tests are EasyGL_ViewportState, EasyGL_Viewport_Subregion and EasyGL_Scissor (disabled, enabled, disabled again). Older descriptions that the SpriteBatch reset the viewport to the full target and never restored it are stale.
Two-sided stencil: one mask, two operation sets
A reading of ApplyDepthStencilState() shows the same stencilMask passed to the function call for both faces, which looks like a defect and was once listed as one. It is correct: Microsoft XNA 4.0's DepthStencilState has separate clockwise and counter-clockwise stencil functions and operations but exactly one StencilMask and one StencilWriteMask, so there is no per-face mask to vary. What must be independent is the operation set, and it is:
// XNA's clockwise tuple goes to GL_BACK and the CounterClockwise* tuple to GL_FRONT,
// because EasyGL keeps OpenGL's GL_CCW front-face convention.
device.set_stencil_func_separate(CullFace::Back, ToEasyGLCompareFunc(stencilFunc),
referenceStencil, unsigned(stencilMask));
device.set_stencil_op_separate (CullFace::Back, sFail, dFail, pass);
device.set_stencil_mask_separate(CullFace::Back, unsigned(stencilWriteMask));
device.set_stencil_func_separate(CullFace::Front, ToEasyGLCompareFunc(ccwStencilFunc),
referenceStencil, unsigned(stencilMask)); // same read mask
device.set_stencil_op_separate (CullFace::Front, ToEasyGLStencilOp(ccwStencilFail),
ToEasyGLStencilOp(ccwStencilDepthFail),
ToEasyGLStencilOp(ccwStencilPass));
device.set_stencil_mask_separate(CullFace::Front, unsigned(stencilWriteMask)); // same write mask
(Abridged from EasyGLRenderer::ApplyDepthStencilState; namespace qualifiers and local names shortened.) A two-sided technique such as shadow volumes varies the operations per face and needs nothing more. Note the face mapping: with GL's counter-clockwise front faces, XNA's counter-clockwise tuple belongs to GL's front face, the reverse of a naive reading. Since GraphicsDevice.ReferenceStencil is an independent device property, SetReferenceStencil re-issues the stencil function with the remembered function and mask so a new reference applies without a new DepthStencilState. The public stencil suites (EasyGL_DepthStencilState_StencilTwoSided and the shared stencil matrix) exercise these paths.
Buffers and readback
Vertex and index buffer GetData are answered from the resources' CPU shadows on every renderer, EasyGL included; custom-layout data uploaded through SetDataRaw is read back with GetDataRawEXT. The rules are on The renderer contract: buffers. EasyGL's own texture readback returns exact DXT blocks for compressed textures and reads render targets from their resolved colour texture; the profile-level overview is on EasyGL profiles: transfers and state.
Evidence and limits
Every behaviour above was read in EasyGLRenderer.cpp, its header, GlProfile.hpp and the shared GL headers at 009d40f5; the named tests exist and were not executed for this page. The historical states are included only where they explain current code or a still-useful testing lesson.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Renderers: the GL family · Tutorial 102: the OpenGL family · Graphics state: DepthStencilState
- Architecture
- Graphics architecture: the two OpenGL families
- Maintainer workflow
- Fix a renderer bug
- Tests and validation
- Test architecture: GPU tests