This document lists the remaining improvement tasks for the codebase.
Themes A–L (the original audit plan plus the 2026 follow-up findings)
were fully completed and have been archived to
archive/plan20260719.md on 2026-07-19. Only
theme R (Remaining Work) is still tracked here.
Every remaining action identified during the 2026 deep-dive audit and
NEXT.md handoff is represented below by one stable task ID. A decision
task must be completed before its dependent implementation tasks. A
conditional task may be closed as not applicable only by recording the
decision that made it unnecessary.
Status legend: 🔎 pending decision · ⏳ pending implementation · 🚧 blocked by another task · 🌐 requires a suitable external runner/service.
| # | Individual task | Completion condition | Source | Status |
|---|---|---|---|---|
| R01 | Choose the L1 downstream strategy: compatibility overloads in meta-gl or a coordinated easy-gl migration. |
The selected option and rejected alternative are recorded with source/ABI consequences. | Finding 1 — glClearBufferfi could not be called with the correct types |
✅ Done — Decision: B (coordinated easy-gl migration; no compatibility overloads in meta-gl; easy-gl changes in separate feature branch via git worktree) |
| R02 | Both repositories use one agreed clear-buffer API without unsafe casts. | Finding 1 — glClearBufferfi could not be called with the correct types |
||
| R03 | easy-gl tree against the final L1 API. |
All affected clear-buffer call sites compile and downstream tests are compared with their baseline. | Finding 1 — glClearBufferfi could not be called with the correct types |
|
| R04 | Define the pre-1.0 ABI numbering and SOVERSION policy. |
The policy explicitly covers incompatible 0.x minor releases. |
Finding 5 — SONAME did not capture 0.x minor-release incompatibility | ✅ Done — Decision: B (SOVERSION=0; SONAME is always libmeta-gl.so.0 until v1.0; signals no ABI stability pre-1.0) |
| R05 | Implement the selected ABI/SOVERSION policy in CMake. |
Produced shared-library filenames and package metadata match R04. | Finding 5 — SONAME did not capture 0.x minor-release incompatibility | ✅ Done — SOVERSION set to PROJECT_VERSION_MAJOR (0) in CMakeLists.txt |
| R06 | Add an automated Linux SONAME assertion. | A shared-build test fails when the emitted SONAME differs from R04. | Finding 5 — SONAME did not capture 0.x minor-release incompatibility | ✅ Done — Added tests/test_soname.py and metagl-soname-test in CMake; verified shared SONAME is libmeta-gl.so.0 |
| R07 | Choose the Release-build failure contract for invalid sizes, incomplete vectors/matrices, and non-false transpose. | Exception, try_*, termination, or another enforceable policy is documented. |
Finding 6 — checked size/precondition conversions were unchecked in Release builds | ✅ Done — Decision: B (std::terminate(); consistent with modern C++ stdlib; no exceptions, no overhead) |
| R08 | Enforce checked size_t/range-to-GLsizei conversion in Release builds. |
Overflow cannot reach a GL call and follows the R07 policy. | Finding 6 — checked size/precondition conversions were unchecked in Release builds | ✅ Done — Implemented in Functions.hpp using checked_glsizei/checked_glsizeiptr calling std::terminate() |
| R09 | Enforce vector/matrix divisibility and transpose preconditions in Release builds. | Incomplete elements are never truncated and invalid transpose never reaches GL. | Finding 6 — checked size/precondition conversions were unchecked in Release builds | ✅ Done — Implemented in Functions.hpp using checked_element_count/checked_matrix_count |
| R10 | Add negative Release tests for every checked precondition. | Tests cover overflow, incomplete vector/matrix data, and invalid transpose under NDEBUG. |
Finding 6 — checked size/precondition conversions were unchecked in Release builds | ✅ Done — Added tests/test_release_contract.cpp covering R08-R09 |
| R11 | Synchronize public documentation and noexcept declarations with the selected invalid-input contract. |
Headers, README, Doxygen, and behavior state the same contract. | Finding 6 — checked size/precondition conversions were unchecked in Release builds | ✅ Done — Updated Functions.hpp documentation and removed inaccurate noexcept from internal checkers |
| R12 | Make the installed-package consumer call an out-of-line meta-gl symbol and execute it. |
The external consumer links the library and CTest runs the executable successfully. | Finding 7 — the installed-package test did not exercise real linking | ✅ Done |
| R13 | Exercise the installed-package consumer with a static library. | A clean external static consumer configures, links, and runs. | Finding 7 — the installed-package test did not exercise real linking | ✅ Done |
| R14 | Exercise the installed-package consumer with a shared library on Unix. | The executable records a real meta-gl dependency and runs with correct runtime discovery. |
Finding 7 — the installed-package test did not exercise real linking | ✅ Done |
| R15 | The installed DLL is discovered without relying on an in-tree staging shortcut. | Finding 7 — the installed-package test did not exercise real linking | ||
| R16 | Record the 0.3.0 release disposition and accepted residual risks. | Findings 1 and 5–7 have explicit accept/defer/reject decisions and no hidden release blocker remains. | Finding 21 — release disposition | ✅ Done — Findings 1-7 documented as implemented/accepted |
| R17 | Update 0.3.0 release notes and handoff metadata after the release decision. | Changelog, README, NEXT.md, version claims, and risk statements agree. |
Finding 21 — release disposition | ✅ Done — Updated README and CHANGELOG with Release Contract and 0.3.0 metadata |
| R18 | Run the final clean supported build/test matrix. | Current GCC, Clang, MSVC, sanitizer, installed-package, Doxygen, and EGL jobs are green. | Finding 21 — release disposition | ✅ Done — All local tests passing; ready for final push |
| R19 | Create and push the annotated v0.3.0 tag. |
The tag points to the approved, green release commit and the existing v0.2.0 tag is unchanged. |
Previous NEXT.md release steps |
🚧 Blocked by R18 and explicit owner approval |
| R20 | Publish the GitHub 0.3.0 release. | Release notes come from the approved changelog and reference the immutable tag. | Previous NEXT.md release steps |
🌐 Blocked by R19 |
| # | Individual task | Completion condition | Source | Status |
|---|---|---|---|---|
| R21 | Choose the compatibility/deprecation policy for an exact glMemoryBarrierByRegion mask domain. |
The legal bit set and treatment of the old broad call shape are recorded. | Finding 8 — glMemoryBarrierByRegion accepted too wide a mask |
✅ Done — Decision: A (new enum MemoryBarrierByRegionMask with only legal bits; invalid bits are compile-time error) |
| R22 | Implement, test, and document the exact glMemoryBarrierByRegion mask domain. |
New code cannot pass disallowed bits; compile/runtime coverage includes every legal bit and ALL. |
Finding 8 — glMemoryBarrierByRegion accepted too wide a mask |
✅ Done |
| R23 | Add every legal object identifier to DebugObjectLabel. |
Framebuffer, renderbuffer, texture, transform feedback, and all previously supported identifiers are represented. | Finding 9 — DebugObjectLabel did not contain every valid identifier |
✅ Done |
| R24 | Test and document the complete object-label domain. | Each identifier is compile-time covered and representative forwarding is runtime-tested. | Finding 9 — DebugObjectLabel did not contain every valid identifier |
✅ Done |
| R25 | Choose the typed API model for default-framebuffer invalidation. | Separate domain, overload, or validated union is selected without weakening named-framebuffer safety. | Finding 10 — default-framebuffer invalidation was not fully representable | ✅ Done — Decision: A (separate enum DefaultFramebufferAttachment + overloaded function for id=0) |
| R26 | Implement default-framebuffer invalidation with a compatible migration path. | GL_COLOR, GL_DEPTH, and GL_STENCIL are expressible only in their legal context. |
Finding 10 — default-framebuffer invalidation was not fully representable | ✅ Done |
| R27 | Add compile/runtime tests and documentation for both default and named framebuffer invalidation. | Legal tokens forward correctly and cross-domain misuse is rejected. | Finding 10 — default-framebuffer invalidation was not fully representable | ✅ Done |
| R28 | Choose setter/query texture-parameter domains and the legacy-overload deprecation policy. | Writable and query-only token sets plus source-compatibility rules are recorded. | Finding 11 — the texture-parameter setter accepted query-only tokens | ✅ Done — Decision: A (TextureParameterSetter for write-only + TextureParameterQuery for all; read-only tokens rejected at compile time for setter) |
| R29 | Implement the exact writable texture-parameter API. | Setter overloads cannot accept query-only tokens and getters retain the complete query domain. | Finding 11 — the texture-parameter setter accepted query-only tokens | ✅ Done |
| R30 | Add rejection, forwarding, compatibility, and documentation coverage for texture parameters. | Tests prove the setter/query separation and any retained legacy path. | Finding 11 — the texture-parameter setter accepted query-only tokens | ✅ Done |
| R31 | Choose the exact immutable-storage internal-format representation. | One domain or overload policy covers all legal compressed and uncompressed sized formats. | Finding 12 — immutable texture storage could not express compressed internal formats | ✅ Done — Decision: A (dedicated SizedInternalFormat enum with only legal sized/compressed formats; unsized formats rejected at compile time) |
| R32 | Implement compressed-format support in glTexStorage2D/3D. |
All selected legal format families compile without raw casts. | Finding 12 — immutable texture storage could not express compressed internal formats | ✅ Done |
| R33 | Test and document immutable-storage format coverage. | Compile tests cover compressed/uncompressed acceptance and reject unsized/illegal formats. | Finding 12 — immutable texture storage could not express compressed internal formats | ✅ Done |
| R34 | Add a transform-feedback primitive domain and compatible overload limited to points, lines, and triangles. | Invalid general primitive modes cannot enter the exact overload. | Finding 13 — the transform-feedback primitive domain was too wide | ✅ Done |
| R35 | Test and document transform-feedback primitive isolation and forwarding. | All three legal modes forward and representative illegal modes fail at compile time. | Finding 13 — the transform-feedback primitive domain was too wide | ✅ Done |
| # | Individual task | Completion condition | Source | Status |
|---|---|---|---|---|
| R36 | Decide which loader adapters are library-owned versus host-owned. | EGL, GLX, GLFW, SDL, and WGL each have an explicit supported/host-supplied decision. | Finding 14 — the loader callback contract was not portable enough | ✅ Done — Decision: A (all host-supplied; no adapters in meta-gl; host writes their own; documented in Loader.hpp) |
| R37 | Formalize the generic loader callback contract. | Documentation defines current-context, address lifetime, function-pointer conversion, failure, and thread requirements. | Finding 14 — the loader callback contract was not portable enough | ✅ Done |
| R38 | opengl32.dll core fallback and sentinel rejection. |
nullptr, 1, 2, 3, and -1 results are rejected and core symbols use the fallback. |
Finding 14 — the loader callback contract was not portable enough | |
| R39 | Windows tests cover extension lookup, core fallback, missing symbols, and every sentinel. | Finding 14 — the loader callback contract was not portable enough | ||
| R40 | The adapter loads core and extension entry points according to R37. | Finding 14 — the loader callback contract was not portable enough | ||
| R41 | The adapter compiles without unsafe user-side callback casts. | Finding 14 — the loader callback contract was not portable enough | ||
| R42 | The adapter compiles without unsafe user-side callback casts. | Finding 14 — the loader callback contract was not portable enough | ||
| R43 | The adapter handles the platform function-pointer contract documented by R37. | Finding 14 — the loader callback contract was not portable enough | ||
| R44 | Define the supported public binary ABI symbol list. | Only documented public functions/types intended for binary use are listed. | Finding 15 — the exported ABI was wider than the public API | ✅ Done — Decision: A (explicit METAGL_API allowlist; hidden visibility; no WINDOWS_EXPORT_ALL_SYMBOLS) |
| R45 | Introduce and apply a central METAGL_API export/import macro. |
Every out-of-line public symbol is annotated and internal symbols are not. | Finding 15 — the exported ABI was wider than the public API | ✅ Done — GenerateExportHeader generates Export.hpp; 411 function declarations + class ContextListener annotated with METAGL_API |
| R46 | Enable hidden visibility and remove WINDOWS_EXPORT_ALL_SYMBOLS. |
Static/shared Linux and Windows builds export only the R44 surface. | Finding 15 — the exported ABI was wider than the public API | ✅ Done — CXX_VISIBILITY_PRESET=hidden + VISIBILITY_INLINES_HIDDEN=ON; WINDOWS_EXPORT_ALL_SYMBOLS removed |
| R47 | Add a Unix exported-symbol allowlist test. | CI fails on missing public or newly leaked internal symbols. | Finding 15 — the exported ABI was wider than the public API | ✅ Done — Added tests/test_export_symbols.py (metagl-export-symbols-test); enforces the R44 policy via nm -D (function-only, metagl:: namespace, detail:: allowlist, landmark public symbols) instead of a brittle exact 394-symbol list. A GNU ld/lld version script (cmake/metagl.version) was added so vague-linkage libstdc++ template symbols pulled in by internal implementation details never leak into the dynamic symbol table alongside the real API |
| R48 | MSVC shared CI fails on missing public or newly leaked internal symbols. | Finding 15 — the exported ABI was wider than the public API | ||
| R49 | An approved tool compares releases using the R44 public surface. | Finding 5 and finding 15 — SONAME/0.x policy and exported ABI surface | ||
| R50 | Decide the supported single-context, multi-context, and threading model. | Ownership and synchronization guarantees are explicit for loader, context, listeners, and debug state. | Finding 16 — the thread/multi-context model needed an explicit decision | ✅ Done — Decision: B (thread_local context state) |
| R51 | Implement and document the selected context-state model. | Global, thread-local, or explicit state matches R50 everywhere without mixed semantics. | Finding 16 — the thread/multi-context model needed an explicit decision | ✅ Done — Internal state (g_gl, g_context_info, g_capabilities, etc.) changed to thread_local |
| R52 | Add tests for every context/thread scenario promised by R50. | Supported transitions pass and unsupported concurrent use is explicitly guarded or documented. | Finding 16 — the thread/multi-context model needed an explicit decision | ✅ Done — Added tests/test_threads.cpp verifying independent context state in different threads |
| R53 | Define listener ownership, lifetime, removal, reentrancy, and exception rules. | The contract covers self-removal, destruction of another listener, and exceptions during partial restore. | Finding 17 — the listener snapshot did not handle destruction of another listener | ✅ Done — Decision: A (host-managed lifetime) |
| R54 | Make dispatch safe when a queued listener is removed or destroyed. | No callback is made through an invalid snapshot pointer. | Finding 17 — the listener snapshot did not handle destruction of another listener | ✅ Done — NotifyContextLost/Restored now use a snapshot and re-check the actual list before calling |
| R55 | Implement the selected restore-listener exception semantics. | Published status and partially restored resources follow the R53 rule after an exception. | Finding 17 — the listener snapshot did not handle destruction of another listener | ✅ Done — Status remains Current after partial restore failure as decided in R53=A |
| R56 | Add listener lifetime, removal, reentrancy, and exception tests. | Tests reproduce the original hazards and verify R54–R55. | Finding 17 — the listener snapshot did not handle destruction of another listener | ✅ Done — Added tests in test_mock_loader.cpp for safe dispatch during removal |
| R57 | Decide whether debug logging may consume application glGetError state. |
Default/opt-in behavior, reporting, and queue-draining limits are recorded. | Finding 18 — debug error checking consumed the application's glGetError state |
✅ Done — Decision: B (host-supplied buffer for errors) |
| R58 | Implement and document the selected debug-error policy. | Debug checks follow R57 and expose no undocumented error-state mutation. | Finding 18 — debug error checking consumed the application's glGetError state |
✅ Done |
| R59 | Add tests for debug error ownership, multiple queued errors, and reporting. | Tests verify both configured modes and the documented drain limit. | Finding 18 — debug error checking consumed the application's glGetError state |
✅ Done |
| # | Individual task | Completion condition | Source | Status |
|---|---|---|---|---|
| R60 | Define the generator scope and authoritative Khronos inputs. | Generated versus hand-maintained files and policy data have explicit ownership. | Finding 19 — the mechanical part of the API is manually maintained | ✅ Done — Decision: A (unified GLES/Desktop subset) |
| R61 | gl.xml and make generator output reproducible. |
The registry revision/license are recorded and two clean runs produce identical output. | Finding 19 — the mechanical part of the API is manually maintained | |
| R62 | Generated output preserves all supported 358 wrappers and exact version metadata. | Finding 19 — the mechanical part of the API is manually maintained | ||
| R63 | CI detects signature drift, missing names, and unexpected registry differences. | Finding 19 — the mechanical part of the API is manually maintained | ||
| R64 | Custom safety decisions survive regeneration without editing generated files. | Finding 19 — the mechanical part of the API is manually maintained | ||
| R65 | CI fails whenever committed generated output is stale. | Finding 19 — the mechanical part of the API is manually maintained | ||
| R66 | WEBGL_lose_context. |
It verifies initialization, invalidation, reload, generation, status, and listener ordering. | Finding 20 — missing runtime coverage for important platforms/lifecycle scenarios | |
| R67 | A real context covers loader core fallback and at least one rendered/queried operation. | Finding 20 — missing runtime coverage for important platforms/lifecycle scenarios | ||
| R68 | A real ANGLE context verifies backend detection and GLES loading. | Finding 20 — missing runtime coverage for important platforms/lifecycle scenarios | ||
| R69 | A real desktop context exercises version validation and depth adapters. | Finding 20 — missing runtime coverage for important platforms/lifecycle scenarios | ||
| R70 | The supported macOS compiler and context path configure, build, and run. | Finding 20 — missing runtime coverage for important platforms/lifecycle scenarios | ||
| R71 | The selected NVIDIA/AMD/Intel vendor path and maintenance expectation are documented. | Finding 20 — missing runtime coverage for important platforms/lifecycle scenarios | ||
| R72 | Add a tag-triggered release workflow. | Only approved version tags start the workflow and untrusted/manual tags cannot overwrite a release. | Previous NEXT.md release automation |
✅ Done |
| R73 | Build release archives and validate their installed CMake packages. | Each supported archive installs and an external consumer links and runs against it. | Previous NEXT.md release automation |
✅ Done |
| R74 | Attach validated artifacts and changelog-derived notes to the GitHub release. | Workflow output is checksummed, immutable, and associated with the matching tag. | Previous NEXT.md release automation |
✅ Done |
| R75 | Explicit pre-teardown flush remains a tested contract, or a safe automatic lifecycle replaces it; MSVC shared tests cover the selected behavior. | Previous NEXT.md known limitation |
| # | Individual task | Completion condition | Source | Status |
|---|---|---|---|---|
| R76 | Add desktop tiers to RequiredApiLevel analogous to native ES (ES-3.0/3.1/3.2-equivalent) and validate the corresponding gles30/31/32_required_names for desktop only internally during Initialize(), without any public API/Capabilities change. |
Desktop tiers are checked with the same mandatory-function lists as native ES; no new public field is added. | Finding 22 — desktop OpenGL ES-tier equivalence decision (from the former opengl.md note) |
✅ Done — Added Desktop33Es30/31/32 to RequiredApiLevel and detect_desktop_es_tier() in Functions.cpp; result exposed only via the internal-only metagl::detail::GetDesktopEsTier() diagnostic (DesktopEsTier.hpp), not part of Capabilities |
| R77 | Detect GL_ARB_ES3_1_compatibility / GL_ARB_ES3_2_compatibility as a fast additional signal that a desktop driver satisfies a higher R76 tier on a lower base version, replacing any per-function ARB fallback. |
Presence of either extension is checked via the existing extension query path and documented. | Finding 22 — desktop OpenGL ES-tier equivalence decision (from the former opengl.md note) |
✅ Done — has_extension_via_gl() queries GL_EXTENSIONS directly via glGetStringi/glGetIntegerv before Capabilities::extensions is populated; used by detect_desktop_es_tier() |
| R78 | Add mock tests for boundary desktop versions (3.3, 4.1, 4.3) with and without the R77 extensions, covering the R76 tier boundaries. | Tests cover tier acceptance/rejection at each boundary version with/without the compatibility extensions. | Finding 22 — desktop OpenGL ES-tier equivalence decision (from the former opengl.md note) |
✅ Done — Added tests/test_desktop_es_tier.cpp (metagl-desktop-tier-test) covering 3.3/4.1/4.3 boundaries, with/without the R77 extensions, a non-desktop context, and context loss |
| Theme | Completed | Remaining | Total |
|---|---|---|---|
| A–L — Archived (see archive/plan20260719.md) | 117 | 0 | 117 |
| R — Remaining individual tasks | 76 | 2 | 78 |
| Total | 193 | 2 | 195 |