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 so app shader startup asks the backend for desktop GL/GLES/WebGL policy
instead of carrying local platform branches. It also owns headless-tested instead of carrying local platform branches. It also owns headless-tested
OpenGL extension enumeration through `query_opengl_extensions`, moving the 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 - `pp_legacy_ui_core` is an object-library containment boundary because the
retained base `Node` controls still depend on legacy renderer and app retained base `Node` controls still depend on legacy renderer and app
headers. It should shrink as layout parsing, colors, generic controls, and 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-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-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-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-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-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 | | 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 dispatch-tested `query_opengl_extensions` helper; shader startup still logs and
applies the resulting feature flags, but the GL extension query loop is no applies the resulting feature flags, but the GL extension query loop is no
longer app-owned. 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, Prepared-file save/download handoff is now also part of the service contract,
so iOS/Web export completion routes through `PlatformServices` after the app so iOS/Web export completion routes through `PlatformServices` after the app
writes the temporary/exported payload. writes the temporary/exported payload.

View File

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

View File

@@ -27,6 +27,16 @@ struct OpenGlCapabilities {
bool float16_textures = false; bool float16_textures = false;
}; };
struct OpenGlFeatureState {
OpenGlCapabilities capabilities;
pp::renderer::RenderDeviceFeatures features;
};
struct OpenGlCapabilityDetectionResult {
std::vector<std::string> extensions;
OpenGlFeatureState feature_state;
};
struct OpenGlPixelFormat { struct OpenGlPixelFormat {
std::uint32_t internal_format = 0; std::uint32_t internal_format = 0;
std::uint32_t pixel_format = 0; std::uint32_t pixel_format = 0;
@@ -689,6 +699,12 @@ struct OpenGlMeshDeleteDispatch {
[[nodiscard]] OpenGlRuntime opengl_runtime_for_current_build() noexcept; [[nodiscard]] OpenGlRuntime opengl_runtime_for_current_build() noexcept;
[[nodiscard]] pp::renderer::RenderDeviceFeatures render_device_features( [[nodiscard]] pp::renderer::RenderDeviceFeatures render_device_features(
OpenGlCapabilities capabilities) noexcept; 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]] OpenGlInitialState panopainter_initial_state() noexcept;
[[nodiscard]] pp::foundation::Status apply_panopainter_initial_state(OpenGlStateDispatch dispatch) noexcept; [[nodiscard]] pp::foundation::Status apply_panopainter_initial_state(OpenGlStateDispatch dispatch) noexcept;
[[nodiscard]] pp::foundation::Result<OpenGlSavedState> snapshot_opengl_state( [[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); 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) void detects_gles_texture_float_extensions(pp::tests::Harness& h)
{ {
constexpr std::array<std::string_view, 3> extensions { 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); 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) void clears_app_default_target(pp::tests::Harness& h)
{ {
recorded_clear_calls.clear(); recorded_clear_calls.clear();
@@ -4185,6 +4271,8 @@ int main()
pp::tests::Harness harness; pp::tests::Harness harness;
harness.run("detects_common_extension_capabilities", detects_common_extension_capabilities); 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("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("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("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); 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("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("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("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("clears_app_default_target", clears_app_default_target);
harness.run("rejects_incomplete_app_clear_dispatch", rejects_incomplete_app_clear_dispatch); harness.run("rejects_incomplete_app_clear_dispatch", rejects_incomplete_app_clear_dispatch);
harness.run("applies_viewport_dispatch", applies_viewport_dispatch); harness.run("applies_viewport_dispatch", applies_viewport_dispatch);