From 155e67fcec007c12c54e5b5b6cdc32ddaafac2cc Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 05:56:57 +0200 Subject: [PATCH] Route app viewport scissor through renderer GL --- docs/modernization/build-inventory.md | 6 +- docs/modernization/roadmap.md | 4 + src/app.cpp | 79 +++++++++-- src/renderer_gl/opengl_capabilities.cpp | 45 +++++++ src/renderer_gl/opengl_capabilities.h | 26 ++++ tests/renderer_gl/capabilities_tests.cpp | 159 +++++++++++++++++++++++ 6 files changed, 305 insertions(+), 14 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 98a3f78..16087f4 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -462,8 +462,10 @@ Known local toolchain state: - `pp_renderer_gl` owns the tested `OpenGlInitialState` startup depth/blend policy and dispatch application consumed by `App::init`, tested runtime version/vendor/renderer/GLSL string query dispatch, tested default clear - color/buffer dispatch consumed by `App::clear`, plus renderer API to OpenGL - token mapping and command-planning contracts used by the OpenGL parity work. + color/buffer dispatch consumed by `App::clear`, tested app UI + viewport/scissor dispatch consumed by `App::draw`, plus renderer API to + OpenGL token mapping and command-planning contracts used by the OpenGL parity + work. - `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability, new-document warning, publish prompt, and save-before-upload planning as JSON; the live cloud upload command consumes the same start contract before diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 1c860b2..5286f36 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -524,6 +524,10 @@ result while the backend owns the query set and order. The default app clear color and color-buffer clear operation now dispatch through `pp_renderer_gl` as well, moving another direct OpenGL operation out of `App::clear` while preserving the current gray clear behavior. +Main app UI viewport and scissor execution now dispatch through tested +`pp_renderer_gl` viewport/scissor contracts, leaving `App::draw` and UI node +clipping to provide rectangles while the backend owns scissor-state tokens and +the live OpenGL call sequence. Windows RenderDoc frame capture hooks now also dispatch through `PlatformServices`, keeping capture integration in the platform service while leaving non-Windows adapters as no-ops. diff --git a/src/app.cpp b/src/app.cpp index adc3ac4..a6c23c0 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -25,11 +25,6 @@ std::condition_variable App::render_cv; namespace { -[[nodiscard]] GLenum scissor_test_state() noexcept -{ - return static_cast(pp::renderer::gl::scissor_test_state()); -} - [[nodiscard]] const char* query_opengl_string(std::uint32_t name) noexcept { return reinterpret_cast(glGetString(static_cast(name))); @@ -65,6 +60,52 @@ void clear_opengl_buffers(std::uint32_t mask) noexcept glClear(static_cast(mask)); } +void set_opengl_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept +{ + glViewport(static_cast(x), static_cast(y), static_cast(width), static_cast(height)); +} + +void set_opengl_scissor(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept +{ + glScissor(static_cast(x), static_cast(y), static_cast(width), static_cast(height)); +} + +void apply_app_viewport(pp::renderer::gl::OpenGlViewportRect viewport) +{ + const auto status = pp::renderer::gl::apply_opengl_viewport( + viewport, + pp::renderer::gl::OpenGlViewportDispatch { + .viewport = set_opengl_viewport, + }); + if (!status.ok()) + LOG("OpenGL viewport failed: %s", status.message); +} + +void apply_app_scissor(pp::renderer::gl::OpenGlScissorRect scissor) +{ + const auto status = pp::renderer::gl::apply_opengl_scissor_rect( + scissor, + pp::renderer::gl::OpenGlScissorDispatch { + .enable = enable_opengl_state, + .disable = disable_opengl_state, + .scissor = set_opengl_scissor, + }); + if (!status.ok()) + LOG("OpenGL scissor failed: %s", status.message); +} + +void apply_app_scissor_test(bool enabled) +{ + const auto status = pp::renderer::gl::apply_opengl_scissor_test( + enabled, + pp::renderer::gl::OpenGlScissorTestDispatch { + .enable = enable_opengl_state, + .disable = disable_opengl_state, + }); + if (!status.ok()) + LOG("OpenGL scissor test failed: %s", status.message); +} + [[nodiscard]] GLint rgba8_internal_format() noexcept { return static_cast(pp::renderer::gl::rgba8_internal_format()); @@ -512,7 +553,13 @@ bool App::update_ui_observer(Node *n) n->m_on_screen = true; } glm::ivec4 c = glm::vec4(box.x - 1, (height / zoom - box.y - box.w - 1), box.z + 2, box.w + 2) * zoom; - glScissor(floorf(c.x + off_x), floorf(c.y + off_y), ceilf(c.z), ceilf(c.w)); + apply_app_scissor(pp::renderer::gl::OpenGlScissorRect { + .enabled = 1U, + .x = static_cast(floorf(c.x + off_x)), + .y = static_cast(floorf(c.y + off_y)), + .width = static_cast(ceilf(c.z)), + .height = static_cast(ceilf(c.w)), + }); n->draw(); return true; } @@ -531,28 +578,36 @@ void App::draw(float dt) { uirtt.bindFramebuffer(); uirtt.clear(); - glViewport(0, 0, uirtt.getWidth(), uirtt.getHeight()); - glEnable(scissor_test_state()); + apply_app_viewport(pp::renderer::gl::OpenGlViewportRect { + .width = static_cast(uirtt.getWidth()), + .height = static_cast(uirtt.getHeight()), + }); + apply_app_scissor_test(true); for (int i = 1; i < layout[main_id]->m_children.size(); i++) layout[main_id]->m_children[i]->watch(observer); for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++) layout_designer[main_id]->m_children[i]->watch(observer); //msgbox->watch(observer); - glDisable(scissor_test_state()); + apply_app_scissor_test(false); uirtt.unbindFramebuffer(); } if (!vr_only) { bind_main_render_target(); - glViewport(off_x, off_y, (GLsizei)width, (GLsizei)height); - glEnable(scissor_test_state()); + apply_app_viewport(pp::renderer::gl::OpenGlViewportRect { + .x = static_cast(off_x), + .y = static_cast(off_y), + .width = static_cast(width), + .height = static_cast(height), + }); + apply_app_scissor_test(true); for (int i = 0; i < layout[main_id]->m_children.size(); i++) layout[main_id]->m_children[i]->watch(observer); for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++) layout_designer[main_id]->m_children[i]->watch(observer); //msgbox->watch(observer); - glDisable(scissor_test_state()); + apply_app_scissor_test(false); } redraw = false; diff --git a/src/renderer_gl/opengl_capabilities.cpp b/src/renderer_gl/opengl_capabilities.cpp index 894bcd3..ac37aa2 100644 --- a/src/renderer_gl/opengl_capabilities.cpp +++ b/src/renderer_gl/opengl_capabilities.cpp @@ -281,6 +281,51 @@ pp::foundation::Status clear_panopainter_default_target(OpenGlClearDispatch disp return pp::foundation::Status::success(); } +pp::foundation::Status apply_opengl_viewport( + OpenGlViewportRect viewport, + OpenGlViewportDispatch dispatch) noexcept +{ + if (dispatch.viewport == nullptr) { + return pp::foundation::Status::invalid_argument("OpenGL viewport dispatch callback must not be null"); + } + + dispatch.viewport(viewport.x, viewport.y, viewport.width, viewport.height); + return pp::foundation::Status::success(); +} + +pp::foundation::Status apply_opengl_scissor_rect( + OpenGlScissorRect scissor, + OpenGlScissorDispatch dispatch) noexcept +{ + if (dispatch.enable == nullptr || dispatch.disable == nullptr || dispatch.scissor == nullptr) { + return pp::foundation::Status::invalid_argument("OpenGL scissor dispatch callbacks must not be null"); + } + + if (scissor.enabled != 0U) { + dispatch.enable(scissor_test_state()); + dispatch.scissor(scissor.x, scissor.y, scissor.width, scissor.height); + } else { + dispatch.disable(scissor_test_state()); + } + return pp::foundation::Status::success(); +} + +pp::foundation::Status apply_opengl_scissor_test( + bool enabled, + OpenGlScissorTestDispatch dispatch) noexcept +{ + if (dispatch.enable == nullptr || dispatch.disable == nullptr) { + return pp::foundation::Status::invalid_argument("OpenGL scissor test dispatch callbacks must not be null"); + } + + if (enabled) { + dispatch.enable(scissor_test_state()); + } else { + dispatch.disable(scissor_test_state()); + } + return pp::foundation::Status::success(); +} + std::uint32_t extension_count_query() noexcept { return gl_num_extensions; diff --git a/src/renderer_gl/opengl_capabilities.h b/src/renderer_gl/opengl_capabilities.h index 6da45b2..ce8ebe6 100644 --- a/src/renderer_gl/opengl_capabilities.h +++ b/src/renderer_gl/opengl_capabilities.h @@ -155,12 +155,29 @@ struct OpenGlDefaultClear { using OpenGlClearColorFn = void (*)(float r, float g, float b, float a) noexcept; using OpenGlClearFn = void (*)(std::uint32_t mask) noexcept; +using OpenGlViewportFn = void (*)(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept; +using OpenGlScissorFn = void (*)(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept; struct OpenGlClearDispatch { OpenGlClearColorFn clear_color = nullptr; OpenGlClearFn clear = nullptr; }; +struct OpenGlViewportDispatch { + OpenGlViewportFn viewport = nullptr; +}; + +struct OpenGlScissorDispatch { + OpenGlCapabilityFn enable = nullptr; + OpenGlCapabilityFn disable = nullptr; + OpenGlScissorFn scissor = nullptr; +}; + +struct OpenGlScissorTestDispatch { + OpenGlCapabilityFn enable = nullptr; + OpenGlCapabilityFn disable = nullptr; +}; + [[nodiscard]] OpenGlCapabilities detect_opengl_capabilities( std::span extensions, OpenGlRuntime runtime) noexcept; @@ -172,6 +189,15 @@ struct OpenGlClearDispatch { OpenGlRuntimeInfoDispatch dispatch) noexcept; [[nodiscard]] OpenGlDefaultClear panopainter_default_clear() noexcept; [[nodiscard]] pp::foundation::Status clear_panopainter_default_target(OpenGlClearDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Status apply_opengl_viewport( + OpenGlViewportRect viewport, + OpenGlViewportDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Status apply_opengl_scissor_rect( + OpenGlScissorRect scissor, + OpenGlScissorDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Status apply_opengl_scissor_test( + bool enabled, + OpenGlScissorTestDispatch dispatch) noexcept; [[nodiscard]] std::uint32_t extension_count_query() noexcept; [[nodiscard]] std::uint32_t extension_string_name() noexcept; diff --git a/tests/renderer_gl/capabilities_tests.cpp b/tests/renderer_gl/capabilities_tests.cpp index 76d5364..b5c606a 100644 --- a/tests/renderer_gl/capabilities_tests.cpp +++ b/tests/renderer_gl/capabilities_tests.cpp @@ -27,6 +27,8 @@ struct RecordedOpenGlStateCall { std::vector recorded_state_calls; std::vector recorded_string_queries; std::vector recorded_clear_calls; +std::vector recorded_viewport_calls; +std::vector recorded_scissor_calls; void record_enable(std::uint32_t state) noexcept { @@ -93,6 +95,27 @@ void record_clear(std::uint32_t mask) noexcept }); } +void record_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept +{ + recorded_viewport_calls.push_back(pp::renderer::gl::OpenGlViewportRect { + .x = x, + .y = y, + .width = width, + .height = height, + }); +} + +void record_scissor(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept +{ + recorded_scissor_calls.push_back(pp::renderer::gl::OpenGlScissorRect { + .enabled = 1U, + .x = x, + .y = y, + .width = width, + .height = height, + }); +} + void detects_common_extension_capabilities(pp::tests::Harness& h) { constexpr std::array extensions { @@ -838,6 +861,136 @@ void rejects_incomplete_app_clear_dispatch(pp::tests::Harness& h) PP_EXPECT(h, status.code == pp::foundation::StatusCode::invalid_argument); } +void applies_viewport_dispatch(pp::tests::Harness& h) +{ + recorded_viewport_calls.clear(); + + const auto status = pp::renderer::gl::apply_opengl_viewport( + pp::renderer::gl::OpenGlViewportRect { + .x = 8, + .y = 16, + .width = 1024, + .height = 512, + }, + pp::renderer::gl::OpenGlViewportDispatch { + .viewport = record_viewport, + }); + + PP_EXPECT(h, status.ok()); + PP_EXPECT(h, recorded_viewport_calls.size() == 1U); + PP_EXPECT(h, recorded_viewport_calls[0].x == 8); + PP_EXPECT(h, recorded_viewport_calls[0].y == 16); + PP_EXPECT(h, recorded_viewport_calls[0].width == 1024); + PP_EXPECT(h, recorded_viewport_calls[0].height == 512); +} + +void rejects_incomplete_viewport_dispatch(pp::tests::Harness& h) +{ + const auto status = pp::renderer::gl::apply_opengl_viewport( + pp::renderer::gl::OpenGlViewportRect { + .width = 1, + .height = 1, + }, + pp::renderer::gl::OpenGlViewportDispatch {}); + + PP_EXPECT(h, !status.ok()); + PP_EXPECT(h, status.code == pp::foundation::StatusCode::invalid_argument); +} + +void applies_scissor_dispatch(pp::tests::Harness& h) +{ + recorded_state_calls.clear(); + recorded_scissor_calls.clear(); + + const auto enabled_status = pp::renderer::gl::apply_opengl_scissor_rect( + pp::renderer::gl::OpenGlScissorRect { + .enabled = 1U, + .x = 4, + .y = 12, + .width = 320, + .height = 200, + }, + pp::renderer::gl::OpenGlScissorDispatch { + .enable = record_enable, + .disable = record_disable, + .scissor = record_scissor, + }); + + const auto disabled_status = pp::renderer::gl::apply_opengl_scissor_rect( + pp::renderer::gl::OpenGlScissorRect {}, + pp::renderer::gl::OpenGlScissorDispatch { + .enable = record_enable, + .disable = record_disable, + .scissor = record_scissor, + }); + + PP_EXPECT(h, enabled_status.ok()); + PP_EXPECT(h, disabled_status.ok()); + PP_EXPECT(h, recorded_state_calls.size() == 2U); + PP_EXPECT(h, recorded_state_calls[0].kind == RecordedOpenGlStateCall::Kind::enable); + PP_EXPECT(h, recorded_state_calls[0].first == 0x0C11U); + PP_EXPECT(h, recorded_state_calls[1].kind == RecordedOpenGlStateCall::Kind::disable); + PP_EXPECT(h, recorded_state_calls[1].first == 0x0C11U); + PP_EXPECT(h, recorded_scissor_calls.size() == 1U); + PP_EXPECT(h, recorded_scissor_calls[0].x == 4); + PP_EXPECT(h, recorded_scissor_calls[0].y == 12); + PP_EXPECT(h, recorded_scissor_calls[0].width == 320); + PP_EXPECT(h, recorded_scissor_calls[0].height == 200); +} + +void rejects_incomplete_scissor_dispatch(pp::tests::Harness& h) +{ + const auto status = pp::renderer::gl::apply_opengl_scissor_rect( + pp::renderer::gl::OpenGlScissorRect { + .enabled = 1U, + }, + pp::renderer::gl::OpenGlScissorDispatch { + .enable = record_enable, + .disable = record_disable, + }); + + PP_EXPECT(h, !status.ok()); + PP_EXPECT(h, status.code == pp::foundation::StatusCode::invalid_argument); +} + +void applies_scissor_test_dispatch(pp::tests::Harness& h) +{ + recorded_state_calls.clear(); + + const auto enabled_status = pp::renderer::gl::apply_opengl_scissor_test( + true, + pp::renderer::gl::OpenGlScissorTestDispatch { + .enable = record_enable, + .disable = record_disable, + }); + const auto disabled_status = pp::renderer::gl::apply_opengl_scissor_test( + false, + pp::renderer::gl::OpenGlScissorTestDispatch { + .enable = record_enable, + .disable = record_disable, + }); + + PP_EXPECT(h, enabled_status.ok()); + PP_EXPECT(h, disabled_status.ok()); + PP_EXPECT(h, recorded_state_calls.size() == 2U); + PP_EXPECT(h, recorded_state_calls[0].kind == RecordedOpenGlStateCall::Kind::enable); + PP_EXPECT(h, recorded_state_calls[0].first == 0x0C11U); + PP_EXPECT(h, recorded_state_calls[1].kind == RecordedOpenGlStateCall::Kind::disable); + PP_EXPECT(h, recorded_state_calls[1].first == 0x0C11U); +} + +void rejects_incomplete_scissor_test_dispatch(pp::tests::Harness& h) +{ + const auto status = pp::renderer::gl::apply_opengl_scissor_test( + true, + pp::renderer::gl::OpenGlScissorTestDispatch { + .enable = record_enable, + }); + + PP_EXPECT(h, !status.ok()); + PP_EXPECT(h, status.code == pp::foundation::StatusCode::invalid_argument); +} + void maps_renderer_viewports_and_scissors(pp::tests::Harness& h) { const auto viewport = pp::renderer::gl::viewport_for_renderer_viewport( @@ -1218,6 +1371,12 @@ int main() harness.run("rejects_incomplete_app_runtime_info_dispatch", rejects_incomplete_app_runtime_info_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); + harness.run("rejects_incomplete_viewport_dispatch", rejects_incomplete_viewport_dispatch); + harness.run("applies_scissor_dispatch", applies_scissor_dispatch); + harness.run("rejects_incomplete_scissor_dispatch", rejects_incomplete_scissor_dispatch); + harness.run("applies_scissor_test_dispatch", applies_scissor_test_dispatch); + harness.run("rejects_incomplete_scissor_test_dispatch", rejects_incomplete_scissor_test_dispatch); harness.run("maps_renderer_viewports_and_scissors", maps_renderer_viewports_and_scissors); harness.run("maps_renderer_blend_state_tokens", maps_renderer_blend_state_tokens); harness.run("maps_renderer_color_write_masks", maps_renderer_color_write_masks);