Move shader feature negotiation into renderer backend

This commit is contained in:
2026-06-04 19:49:06 +02:00
parent 1057dd488a
commit f2cb0f2276
7 changed files with 171 additions and 35 deletions

View File

@@ -793,7 +793,11 @@ Known warnings after the current CMake app build:
so app shader startup asks the backend for desktop GL/GLES/WebGL policy
instead of carrying local platform branches. It also owns headless-tested
OpenGL extension enumeration through `query_opengl_extensions`, moving the
extension count/string query loop out of `app_shaders.cpp`.
extension count/string query loop out of `app_shaders.cpp`. Startup feature
negotiation now uses `query_opengl_capability_detection`, so extension
enumeration, runtime capability policy, and renderer-neutral feature
conversion are validated together before the retained `ShaderManager` static
flags are updated.
- `pp_legacy_ui_core` is an object-library containment boundary because the
retained base `Node` controls still depend on legacy renderer and app
headers. It should shrink as layout parsing, colors, generic controls, and

View File

@@ -53,7 +53,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0033 | Open | Modernization | Tools menu planning and direct command execution dispatch now consume pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, and the `ToolsMenuServices` boundary, and direct command execution is centralized in `src/legacy_app_shell_services.*`; SonarPen availability/startup now routes through `PlatformServices`, but live adapters still construct legacy `NodePanelFloating` panels, mutate legacy panel nodes, clear `CanvasModeGrid`, reset `NodeCanvas` camera state, open legacy shortcuts UI, and rely on the legacy platform adapter for the retained iOS SonarPen bridge | Preserve current Tools menu behavior while UI shell actions move toward app/UI/platform services | `pp_app_core_tools_menu_tests`; `pp_platform_api_tests`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-tools-panel --panel animation --already-visible`; `ctest --preset desktop-fast --build-config Debug` | Tools panel creation, submenu routing, grid clear, camera reset, shortcuts dialog, and SonarPen dispatch are owned by injected app/UI/platform services with `App::init_menu_tools` acting only as a UI adapter and no legacy Tools adapter |
| DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy About/manual/what's-new dialogs, invokes the injected crash hook, and runs the legacy Canvas stroke performance test directly | Preserve About menu behavior while dialogs and diagnostics move toward app/UI/platform services | `pp_app_core_about_menu_tests`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-about-menu --command performance --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | About/manual/what's-new dialog dispatch, crash-test dispatch, and performance-test execution are owned by injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter |
| DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter |
| DEBT-0036 | Open | Modernization | `pp_renderer_api`, `pp_paint_renderer`, `pano_cli plan-paint-feedback`, and `pano_cli plan-stroke-composite` can choose backend-neutral complex paint feedback strategies for fixed-function blending, framebuffer-fetch-capable renderers, or ping-pong render targets. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::render_device_features` as the backend conversion point. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current destination-feedback decision, and live `Canvas::stroke_draw`, thumbnail layer blending, and `NodeStrokePreview` brush-preview rendering use it for framebuffer-fetch versus destination-copy decisions. Actual live stroke rasterization, dual-brush compositing, pattern feedback math, thumbnail layer compositing, and brush-preview compositing still use legacy OpenGL canvas/UI execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Live stroke/layer compositing chooses its feedback path through `pp_paint_renderer` and renderer services, with OpenGL golden parity and Vulkan/Metal lab tests covering framebuffer-fetch and ping-pong behavior |
| DEBT-0036 | Open | Modernization | `pp_renderer_api`, `pp_paint_renderer`, `pano_cli plan-paint-feedback`, and `pano_cli plan-stroke-composite` can choose backend-neutral complex paint feedback strategies for fixed-function blending, framebuffer-fetch-capable renderers, or ping-pong render targets. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::query_opengl_capability_detection`, `detect_opengl_feature_state`, and `render_device_features` as the backend conversion point. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current destination-feedback decision, and live `Canvas::stroke_draw`, thumbnail layer blending, and `NodeStrokePreview` brush-preview rendering use it for framebuffer-fetch versus destination-copy decisions. Actual live stroke rasterization, dual-brush compositing, pattern feedback math, thumbnail layer compositing, and brush-preview compositing still use legacy OpenGL canvas/UI execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Live stroke/layer compositing chooses its feedback path through `pp_paint_renderer` and renderer services, with OpenGL golden parity and Vulkan/Metal lab tests covering framebuffer-fetch and ping-pong behavior |
| DEBT-0037 | Open | Modernization | Recording lifecycle/export planning and execution dispatch now consume pure `pp_app_core` through `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, `pano_cli plan-recording-session`, and the `RecordingServices` boundary; live execution is centralized in `src/legacy_recording_services.*`, but the bridge still owns legacy recording thread startup/shutdown, platform recorded-file cleanup, progress UI, PBO readback through `App::rec_loop`, and `MP4Encoder::write_mp4` execution | Preserve current timelapse/MP4 behavior while recording moves toward app/document/renderer/video services | `pp_app_core_document_recording_tests`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-recording-session --platform-clears-files`; `ctest --preset desktop-fast --build-config Debug` | Recording thread lifecycle, frame readback, platform cleanup, progress reporting, and MP4 writing are owned by injected app/renderer/video services with `App` methods acting only as adapters |
| DEBT-0038 | Open | Modernization | Cloud upload/browse/bulk planning and execution dispatch now consume pure `pp_app_core` through `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-upload-all`, `pano_cli plan-cloud-browse`, and the `CloudServices` boundary; live execution is centralized in `src/legacy_cloud_services.*`, the app-owned `upload`/`download`/license curl helpers now ask `PlatformServices` for the Android TLS-verification bypass policy, and retained `Asset::open_url`, `LogRemote::net_init`, and `NodeDialogCloud::load_thumbs_thread` curl sites consume the `pp_platform_api` default TLS policy helper instead of spelling Android branches locally, but the bridge still uses legacy save-before-upload, app-owned curl helpers instead of an injected network service, progress/message UI, OpenGL context guarding, `NodeDialogCloud`, `Canvas` project open, layer refresh, and `ActionManager` reset | Preserve current cloud behavior while cloud/network/document import flows move toward app/document/platform services | `pp_app_core_document_cloud_tests`; `pp_platform_api_tests`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `ctest --preset desktop-fast --build-config Debug` | Cloud upload/download, TLS policy, save-before-upload, progress reporting, cloud browse dialog, downloaded project opening, layer refresh, OpenGL context ownership, and action-history reset are owned by injected app/document/network/platform/renderer services with `App` methods acting only as adapters |
| DEBT-0039 | Open | Modernization | Document-open planning and execution dispatch now consume pure `pp_app_core` through `App::open_document`, `pano_cli plan-open-route`, `DocumentOpenServices`, and `src/legacy_document_open_services.*`, but the bridge still opens ABR/PPBR import prompts before delegating import execution to `src/legacy_brush_package_import_services.*`, applies unsaved-project discard prompts, calls legacy project-open execution, refreshes layer UI, updates the app title, and clears legacy history directly | Preserve current file-open/import behavior while document loading and brush import move toward app/document/asset/UI services | `pp_app_core_document_route_tests`; `pp_app_core_document_session_tests`; `pano_cli plan-open-route --path D:/Paint/Scenes/demo.ppi --unsaved`; `pano_cli plan-open-route --path D:/Paint/Brushes/clouds.ABR --unsaved`; `ctest --preset desktop-fast --build-config Debug` | Brush import prompting, project-open execution, unsaved-project discard prompting, layer refresh, title updates, and history clearing are owned by injected app/document/asset/UI services with `App::open_document` acting only as an adapter |

View File

@@ -680,6 +680,13 @@ OpenGL extension enumeration now also lives in `pp_renderer_gl` through a
dispatch-tested `query_opengl_extensions` helper; shader startup still logs and
applies the resulting feature flags, but the GL extension query loop is no
longer app-owned.
OpenGL shader startup feature negotiation now also flows through
`pp_renderer_gl::query_opengl_capability_detection` and
`detect_opengl_feature_state`, so extension enumeration, desktop/GLES/WebGL
capability policy, and renderer-neutral feature conversion are tested together
behind the backend boundary. `App::initShaders` remains a legacy adapter that
copies the backend-owned feature snapshot into retained `ShaderManager` static
flags until `ShaderManager` itself becomes an OpenGL backend service.
Prepared-file save/download handoff is now also part of the service contract,
so iOS/Web export completion routes through `PlatformServices` after the app
writes the temporary/exported payload.

View File

@@ -6,15 +6,14 @@
namespace {
[[nodiscard]] pp::renderer::gl::OpenGlCapabilities shader_manager_capabilities() noexcept
void apply_shader_manager_feature_state(pp::renderer::gl::OpenGlFeatureState feature_state) noexcept
{
pp::renderer::gl::OpenGlCapabilities capabilities;
capabilities.framebuffer_fetch = ShaderManager::ext_framebuffer_fetch;
capabilities.map_buffer_alignment = ShaderManager::ext_map_aligned;
capabilities.float32_textures = ShaderManager::ext_float32;
capabilities.float32_linear = ShaderManager::ext_float32_linear;
capabilities.float16_textures = ShaderManager::ext_float16;
return capabilities;
ShaderManager::ext_framebuffer_fetch = feature_state.capabilities.framebuffer_fetch;
ShaderManager::ext_map_aligned = feature_state.capabilities.map_buffer_alignment;
ShaderManager::ext_float32 = feature_state.capabilities.float32_textures;
ShaderManager::ext_float32_linear = feature_state.capabilities.float32_linear;
ShaderManager::ext_float16 = feature_state.capabilities.float16_textures;
ShaderManager::set_render_device_features(feature_state.features);
}
void query_gl_integer(std::uint32_t name, std::int32_t* value) noexcept
@@ -40,42 +39,28 @@ void App::initShaders()
#endif // _DEBUG
render_task([] {
const auto extensions_result = pp::renderer::gl::query_opengl_extensions(
const auto detection_result = pp::renderer::gl::query_opengl_capability_detection(
pp::renderer::gl::OpenGlExtensionQueryDispatch {
.get_integer = query_gl_integer,
.get_string_indexed = query_gl_string_indexed,
});
if (!extensions_result.ok()) {
LOG("OpenGL extension query failed: %s", extensions_result.status().message);
},
pp::renderer::gl::opengl_runtime_for_current_build());
if (!detection_result.ok()) {
LOG("OpenGL capability detection failed: %s", detection_result.status().message);
return;
}
const auto& extension_storage = extensions_result.value();
std::vector<std::string_view> extension_views;
extension_views.reserve(extension_storage.size());
for (const auto& extension : extension_storage) {
extension_views.push_back(extension);
const auto& detection = detection_result.value();
for (const auto& extension : detection.extensions) {
LOG("EXT: %s", extension.c_str());
}
const auto runtime = pp::renderer::gl::opengl_runtime_for_current_build();
const auto capabilities = pp::renderer::gl::detect_opengl_capabilities(extension_views, runtime);
ShaderManager::ext_framebuffer_fetch = capabilities.framebuffer_fetch;
ShaderManager::ext_map_aligned = capabilities.map_buffer_alignment;
ShaderManager::ext_float32 = capabilities.float32_textures;
ShaderManager::ext_float32_linear = capabilities.float32_linear;
ShaderManager::ext_float16 = capabilities.float16_textures;
ShaderManager::set_render_device_features(pp::renderer::gl::render_device_features(capabilities));
apply_shader_manager_feature_state(detection.feature_state);
});
if (pp::renderer::gl::opengl_runtime_for_current_build().desktop_gl) {
// In OpenGL 3.3 these should be already available.
ShaderManager::ext_float32_linear = true;
ShaderManager::ext_float32 = true;
ShaderManager::ext_float16 = true;
}
ShaderManager::set_render_device_features(
pp::renderer::gl::render_device_features(shader_manager_capabilities()));
apply_shader_manager_feature_state(pp::renderer::gl::detect_opengl_feature_state(
std::span<const std::string_view> {},
pp::renderer::gl::opengl_runtime_for_current_build()));
LOG("Shader Extension shader_framebuffer_fetch: %s", ShaderManager::ext_framebuffer_fetch ? "enabled" : "disabled");

View File

@@ -228,6 +228,17 @@ pp::renderer::RenderDeviceFeatures render_device_features(OpenGlCapabilities cap
};
}
OpenGlFeatureState detect_opengl_feature_state(
std::span<const std::string_view> extensions,
OpenGlRuntime runtime) noexcept
{
const auto capabilities = detect_opengl_capabilities(extensions, runtime);
return OpenGlFeatureState {
.capabilities = capabilities,
.features = render_device_features(capabilities),
};
}
OpenGlInitialState panopainter_initial_state() noexcept
{
return OpenGlInitialState {
@@ -369,6 +380,28 @@ pp::foundation::Result<std::vector<std::string>> query_opengl_extensions(
return pp::foundation::Result<std::vector<std::string>>::success(std::move(extensions));
}
pp::foundation::Result<OpenGlCapabilityDetectionResult> query_opengl_capability_detection(
OpenGlExtensionQueryDispatch dispatch,
OpenGlRuntime runtime)
{
auto extensions_result = query_opengl_extensions(dispatch);
if (!extensions_result.ok()) {
return pp::foundation::Result<OpenGlCapabilityDetectionResult>::failure(extensions_result.status());
}
auto extensions = std::move(extensions_result.value());
std::vector<std::string_view> extension_views;
extension_views.reserve(extensions.size());
for (const auto& extension : extensions) {
extension_views.push_back(extension);
}
return pp::foundation::Result<OpenGlCapabilityDetectionResult>::success(OpenGlCapabilityDetectionResult {
.extensions = std::move(extensions),
.feature_state = detect_opengl_feature_state(extension_views, runtime),
});
}
OpenGlDefaultClear panopainter_default_clear() noexcept
{
return OpenGlDefaultClear {

View File

@@ -27,6 +27,16 @@ struct OpenGlCapabilities {
bool float16_textures = false;
};
struct OpenGlFeatureState {
OpenGlCapabilities capabilities;
pp::renderer::RenderDeviceFeatures features;
};
struct OpenGlCapabilityDetectionResult {
std::vector<std::string> extensions;
OpenGlFeatureState feature_state;
};
struct OpenGlPixelFormat {
std::uint32_t internal_format = 0;
std::uint32_t pixel_format = 0;
@@ -689,6 +699,12 @@ struct OpenGlMeshDeleteDispatch {
[[nodiscard]] OpenGlRuntime opengl_runtime_for_current_build() noexcept;
[[nodiscard]] pp::renderer::RenderDeviceFeatures render_device_features(
OpenGlCapabilities capabilities) noexcept;
[[nodiscard]] OpenGlFeatureState detect_opengl_feature_state(
std::span<const std::string_view> extensions,
OpenGlRuntime runtime) noexcept;
[[nodiscard]] pp::foundation::Result<OpenGlCapabilityDetectionResult> query_opengl_capability_detection(
OpenGlExtensionQueryDispatch dispatch,
OpenGlRuntime runtime);
[[nodiscard]] OpenGlInitialState panopainter_initial_state() noexcept;
[[nodiscard]] pp::foundation::Status apply_panopainter_initial_state(OpenGlStateDispatch dispatch) noexcept;
[[nodiscard]] pp::foundation::Result<OpenGlSavedState> snapshot_opengl_state(

View File

@@ -1025,6 +1025,40 @@ void treats_desktop_gl_float_rendering_as_core(pp::tests::Harness& h)
PP_EXPECT(h, features.float32_render_targets);
}
void detects_feature_state_from_extension_capabilities(pp::tests::Harness& h)
{
constexpr std::array<std::string_view, 2> extensions {
"GL_EXT_shader_framebuffer_fetch",
"GL_ARB_map_buffer_alignment",
};
const auto feature_state = pp::renderer::gl::detect_opengl_feature_state(
extensions,
pp::renderer::gl::OpenGlRuntime {});
PP_EXPECT(h, feature_state.capabilities.framebuffer_fetch);
PP_EXPECT(h, feature_state.capabilities.map_buffer_alignment);
PP_EXPECT(h, !feature_state.capabilities.float32_textures);
PP_EXPECT(h, feature_state.features.framebuffer_fetch);
PP_EXPECT(h, feature_state.features.texture_copy);
PP_EXPECT(h, feature_state.features.render_target_blit);
PP_EXPECT(h, !feature_state.features.float32_render_targets);
}
void detects_desktop_feature_state_without_extensions(pp::tests::Harness& h)
{
const auto feature_state = pp::renderer::gl::detect_opengl_feature_state(
{},
pp::renderer::gl::OpenGlRuntime { .desktop_gl = true });
PP_EXPECT(h, !feature_state.capabilities.framebuffer_fetch);
PP_EXPECT(h, feature_state.capabilities.float32_textures);
PP_EXPECT(h, feature_state.capabilities.float32_linear);
PP_EXPECT(h, feature_state.capabilities.float16_textures);
PP_EXPECT(h, feature_state.features.float32_render_targets);
PP_EXPECT(h, feature_state.features.float16_render_targets);
}
void detects_gles_texture_float_extensions(pp::tests::Harness& h)
{
constexpr std::array<std::string_view, 3> extensions {
@@ -1890,6 +1924,58 @@ void rejects_incomplete_extension_query_dispatch(pp::tests::Harness& h)
PP_EXPECT(h, result.status().code == pp::foundation::StatusCode::invalid_argument);
}
void queries_app_capability_detection(pp::tests::Harness& h)
{
recorded_integer_queries.clear();
recorded_indexed_string_queries.clear();
recorded_extension_count = 3;
const auto result = pp::renderer::gl::query_opengl_capability_detection(
pp::renderer::gl::OpenGlExtensionQueryDispatch {
.get_integer = record_get_integer,
.get_string_indexed = record_indexed_string_query,
},
pp::renderer::gl::OpenGlRuntime {});
PP_EXPECT(h, result.ok());
PP_EXPECT(h, recorded_integer_queries.size() == 1U);
PP_EXPECT(h, recorded_integer_queries[0] == 0x821DU);
PP_EXPECT(h, recorded_indexed_string_queries.size() == 3U);
PP_EXPECT(h, result.value().extensions.size() == 3U);
PP_EXPECT(h, result.value().extensions[0] == "GL_EXT_shader_framebuffer_fetch");
PP_EXPECT(h, result.value().feature_state.capabilities.framebuffer_fetch);
PP_EXPECT(h, result.value().feature_state.capabilities.map_buffer_alignment);
PP_EXPECT(h, !result.value().feature_state.capabilities.float32_textures);
PP_EXPECT(h, result.value().feature_state.features.framebuffer_fetch);
}
void query_capability_detection_preserves_webgl_float_policy(pp::tests::Harness& h)
{
recorded_extension_count = 3;
const auto result = pp::renderer::gl::query_opengl_capability_detection(
pp::renderer::gl::OpenGlExtensionQueryDispatch {
.get_integer = record_get_integer,
.get_string_indexed = record_indexed_string_query,
},
pp::renderer::gl::OpenGlRuntime { .gles = true, .web = true });
PP_EXPECT(h, result.ok());
PP_EXPECT(h, result.value().feature_state.capabilities.framebuffer_fetch);
PP_EXPECT(h, !result.value().feature_state.capabilities.float32_textures);
PP_EXPECT(h, !result.value().feature_state.features.float32_render_targets);
}
void rejects_incomplete_capability_detection_dispatch(pp::tests::Harness& h)
{
const auto result = pp::renderer::gl::query_opengl_capability_detection(
pp::renderer::gl::OpenGlExtensionQueryDispatch {},
pp::renderer::gl::OpenGlRuntime {});
PP_EXPECT(h, !result.ok());
PP_EXPECT(h, result.status().code == pp::foundation::StatusCode::invalid_argument);
}
void clears_app_default_target(pp::tests::Harness& h)
{
recorded_clear_calls.clear();
@@ -4185,6 +4271,8 @@ int main()
pp::tests::Harness harness;
harness.run("detects_common_extension_capabilities", detects_common_extension_capabilities);
harness.run("treats_desktop_gl_float_rendering_as_core", treats_desktop_gl_float_rendering_as_core);
harness.run("detects_feature_state_from_extension_capabilities", detects_feature_state_from_extension_capabilities);
harness.run("detects_desktop_feature_state_without_extensions", detects_desktop_feature_state_without_extensions);
harness.run("detects_gles_texture_float_extensions", detects_gles_texture_float_extensions);
harness.run("ignores_gles_texture_extensions_for_webgl_runtime", ignores_gles_texture_extensions_for_webgl_runtime);
harness.run("classifies_current_build_opengl_runtime", classifies_current_build_opengl_runtime);
@@ -4219,6 +4307,9 @@ int main()
harness.run("converts_null_extension_names_to_empty_strings", converts_null_extension_names_to_empty_strings);
harness.run("treats_negative_extension_count_as_empty", treats_negative_extension_count_as_empty);
harness.run("rejects_incomplete_extension_query_dispatch", rejects_incomplete_extension_query_dispatch);
harness.run("queries_app_capability_detection", queries_app_capability_detection);
harness.run("query_capability_detection_preserves_webgl_float_policy", query_capability_detection_preserves_webgl_float_policy);
harness.run("rejects_incomplete_capability_detection_dispatch", rejects_incomplete_capability_detection_dispatch);
harness.run("clears_app_default_target", clears_app_default_target);
harness.run("rejects_incomplete_app_clear_dispatch", rejects_incomplete_app_clear_dispatch);
harness.run("applies_viewport_dispatch", applies_viewport_dispatch);