diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 5fab4e6..96a2fe8 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -18,6 +18,12 @@ agent or engineer to remove them without reconstructing context from chat. ## Recent Reductions +- 2026-06-12: DEBT-0036 was narrowed again. The opt-in `desktop-gpu` + preset now owns a real OpenGL readback golden gate through + `pp_renderer_gl_gpu_readback_tests`, validating a deterministic 1x1 clear + and `glReadPixels` result against exact RGBA bytes. The first context helper + is Windows/WGL-only and skips clearly on platforms without a helper; macOS + and Linux GPU context helpers plus broader golden coverage remain open. - 2026-06-12: DEBT-0060 was closed. Retained Android standard/Quest/Focus package CMake files no longer generate or prepend a patched `nanort.h` overlay. Android package configure now applies the tracked nanort source diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 4d6b804..f62ea95 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -51,7 +51,7 @@ or temporary adapters live only in chat history. | 4 | Component Split Without Behavior Change | Started | Each extracted target builds and tests | | 5 | Renderer Boundary And OpenGL Parity | Started | OpenGL output matches golden readbacks | | 6 | Platform Alignment | Started | Every supported platform has named validation | -| 7 | Hardening, Coverage, And Breaking-Point Tests | Not started | Each component has edge/failure tests | +| 7 | Hardening, Coverage, And Breaking-Point Tests | Started | Each component has edge/failure tests | | 8 | Future Backend Readiness | Not started | Vulkan/Metal lab targets remain non-default | ## Measurable Task Tracking @@ -1729,6 +1729,14 @@ Gate: ## Phase 7: Hardening, Coverage, And Breaking-Point Tests +Status: started. The first opt-in desktop GPU golden/readback gate now lives in +`pp_renderer_gl_gpu_readback_tests` and is selected by the existing +`desktop-gpu` preset. It creates a tiny desktop OpenGL context, clears to a +deterministic 1x1 red fixture, reads back exact RGBA bytes, and skips with a +clear message when no GPU/context helper is available. The first helper is +Windows/WGL-only; macOS/Linux helpers and broader render golden coverage remain +tracked under `DEBT-0036`. + Goal: tests should try to break components, not only confirm current happy paths. diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index 7cee3eb..c1348ec 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -37,8 +37,8 @@ auditable steps rather than by subjective estimates. | Legacy adapter retirement | 20 | 7 | `legacy_*_services` and singleton bridges are deleted or reduced to trivial composition. | | Renderer boundary and OpenGL parity | 15 | 10 | Live render/export/readback paths execute through renderer interfaces with parity checks. | | Platform and package parity | 10 | 6 | Required platforms have root CMake/package validation and injected platform services. | -| Hardening and future backend readiness | 10 | 0 | Edge, fuzz, golden, stress, and backend-lab gates exist for high-risk paths. | -| **Total** | **100** | **53** | Only completed tasks below may change this number. | +| Hardening and future backend readiness | 10 | 2 | Edge, fuzz, golden, stress, and backend-lab gates exist for high-risk paths. | +| **Total** | **100** | **55** | Only completed tasks below may change this number. | When updating `Current`, add a dated note under "Completed Task Log" with the task id, points moved, validation command, and commit hash. @@ -331,7 +331,7 @@ cmake --build --preset windows-msvc-default --config Debug --target PanoPainter ### RND-004 - Add First Desktop GPU Golden Gate -Status: Blocked +Status: Done Score: +2 hardening and future backend readiness Debt: `DEBT-0036` Scope: `tests/`, `CMakeLists.txt`, renderer test helpers only @@ -526,6 +526,7 @@ Done Checks: | Date | Task | Score Change | Validation | Commit | | --- | --- | ---: | --- | --- | +| 2026-06-12 | RND-004 | +2 hardening and future backend readiness | `ctest --preset desktop-gpu --build-config Debug --output-on-failure`; `ctest --preset desktop-fast --build-config Debug -R "pp_renderer_gl\|pp_paint_renderer" --output-on-failure` | pending | | 2026-06-12 | DEP-002 | +1 build and CMake ownership | `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard` | 648404ee | | 2026-06-12 | RND-002 | +2 renderer boundary and OpenGL parity | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export\|pp_paint_renderer_compositor\|pano_cli_plan_export_snapshot_route\|pano_cli_simulate_document_export" --output-on-failure` | 46fb8ef | | 2026-06-12 | RND-001 | +2 renderer boundary and OpenGL parity | `ctest --preset desktop-fast --build-config Debug -R "pp_app_core_document_export\|pp_paint_renderer_compositor\|pano_cli_plan_export_snapshot_route\|pano_cli_simulate_document_export" --output-on-failure` | 46fb8ef | diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ec802f7..0478cb8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -248,6 +248,23 @@ if(TARGET pp_renderer_gl) add_test(NAME pp_renderer_gl_command_plan_tests COMMAND pp_renderer_gl_command_plan_tests) set_tests_properties(pp_renderer_gl_command_plan_tests PROPERTIES LABELS "renderer;desktop-fast") + + add_executable(pp_renderer_gl_gpu_readback_tests + renderer_gl/gpu_readback_tests.cpp) + target_link_libraries(pp_renderer_gl_gpu_readback_tests PRIVATE + pp_renderer_gl + pp_test_harness) + if(WIN32) + target_link_libraries(pp_renderer_gl_gpu_readback_tests PRIVATE + gdi32 + opengl32 + user32) + endif() + + add_test(NAME pp_renderer_gl_gpu_readback_tests COMMAND pp_renderer_gl_gpu_readback_tests) + set_tests_properties(pp_renderer_gl_gpu_readback_tests PROPERTIES + LABELS "renderer;gpu" + SKIP_REGULAR_EXPRESSION "\\[skip\\]") endif() add_executable(pp_paint_renderer_compositor_tests diff --git a/tests/renderer_gl/gpu_readback_tests.cpp b/tests/renderer_gl/gpu_readback_tests.cpp new file mode 100644 index 0000000..e6559aa --- /dev/null +++ b/tests/renderer_gl/gpu_readback_tests.cpp @@ -0,0 +1,185 @@ +#include "../test_harness.h" + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#endif + +#include +#include +#include + +namespace { + +#if defined(_WIN32) + +class HiddenWglContext { +public: + HiddenWglContext() + { + instance_ = GetModuleHandleW(nullptr); + + WNDCLASSW window_class {}; + window_class.style = CS_OWNDC; + window_class.lpfnWndProc = DefWindowProcW; + window_class.hInstance = instance_; + window_class.lpszClassName = class_name_; + + registered_ = RegisterClassW(&window_class) != 0 || GetLastError() == ERROR_CLASS_ALREADY_EXISTS; + if (!registered_) { + skip_reason_ = "RegisterClassW failed"; + return; + } + + window_ = CreateWindowExW( + WS_EX_TOOLWINDOW, + class_name_, + L"PanoPainter GPU readback test", + WS_POPUP, + 0, + 0, + 16, + 16, + nullptr, + nullptr, + instance_, + nullptr); + if (window_ == nullptr) { + skip_reason_ = "CreateWindowExW failed"; + return; + } + ShowWindow(window_, SW_SHOWNA); + UpdateWindow(window_); + + device_context_ = GetDC(window_); + if (device_context_ == nullptr) { + skip_reason_ = "GetDC failed"; + return; + } + + PIXELFORMATDESCRIPTOR pixel_format {}; + pixel_format.nSize = sizeof(pixel_format); + pixel_format.nVersion = 1; + pixel_format.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; + pixel_format.iPixelType = PFD_TYPE_RGBA; + pixel_format.cColorBits = 24; + pixel_format.cAlphaBits = 8; + pixel_format.cDepthBits = 24; + pixel_format.iLayerType = PFD_MAIN_PLANE; + + const int format = ChoosePixelFormat(device_context_, &pixel_format); + if (format == 0 || SetPixelFormat(device_context_, format, &pixel_format) == FALSE) { + skip_reason_ = "OpenGL pixel format setup failed"; + return; + } + + render_context_ = wglCreateContext(device_context_); + if (render_context_ == nullptr) { + skip_reason_ = "wglCreateContext failed"; + return; + } + + if (wglMakeCurrent(device_context_, render_context_) == FALSE) { + skip_reason_ = "wglMakeCurrent failed"; + return; + } + + ready_ = true; + } + + HiddenWglContext(const HiddenWglContext&) = delete; + HiddenWglContext& operator=(const HiddenWglContext&) = delete; + + ~HiddenWglContext() + { + if (render_context_ != nullptr) { + wglMakeCurrent(nullptr, nullptr); + wglDeleteContext(render_context_); + } + if (window_ != nullptr && device_context_ != nullptr) { + ReleaseDC(window_, device_context_); + } + if (window_ != nullptr) { + DestroyWindow(window_); + } + if (registered_) { + UnregisterClassW(class_name_, instance_); + } + } + + [[nodiscard]] bool ready() const noexcept + { + return ready_; + } + + [[nodiscard]] const char* skip_reason() const noexcept + { + return skip_reason_; + } + +private: + static constexpr const wchar_t* class_name_ = L"PanoPainterGpuReadbackTestWindow"; + + HINSTANCE instance_ = nullptr; + HWND window_ = nullptr; + HDC device_context_ = nullptr; + HGLRC render_context_ = nullptr; + const char* skip_reason_ = "OpenGL context unavailable"; + bool registered_ = false; + bool ready_ = false; +}; + +#endif + +void opengl_clear_readback_matches_fixture(pp::tests::Harness& h) +{ +#if defined(_WIN32) + HiddenWglContext context; + if (!context.ready()) { + std::cout << "[skip] desktop GPU OpenGL readback unavailable: " << context.skip_reason() << "\n"; + return; + } + + glViewport(0, 0, 1, 1); + glDrawBuffer(GL_BACK); + glReadBuffer(GL_BACK); + glClearColor(1.0F, 0.0F, 0.0F, 1.0F); + glClear(GL_COLOR_BUFFER_BIT); + glFinish(); + + std::array pixel {}; + glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data()); + + constexpr std::array expected { + 255, + 0, + 0, + 255, + }; + if (pixel != expected) { + std::cout << "readback rgba: " + << static_cast(pixel[0]) << ", " + << static_cast(pixel[1]) << ", " + << static_cast(pixel[2]) << ", " + << static_cast(pixel[3]) << "\n"; + } + PP_EXPECT(h, pixel == expected); +#else + std::cout << "[skip] desktop GPU OpenGL readback unavailable: no platform context helper\n"; +#endif +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("opengl_clear_readback_matches_fixture", opengl_clear_readback_matches_fixture); + return harness.finish(); +}