From fc208514627a1946d2563c68ee32a46f9ec871f9 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Thu, 4 Jun 2026 21:29:49 +0200 Subject: [PATCH] Route PBO readbacks through GL backend --- docs/modernization/build-inventory.md | 18 +- docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 11 +- src/renderer_gl/opengl_capabilities.cpp | 141 +++++++++++++++ src/renderer_gl/opengl_capabilities.h | 50 ++++++ src/rtt.cpp | 137 +++++++++++---- tests/renderer_gl/capabilities_tests.cpp | 210 +++++++++++++++++++++++ 7 files changed, 523 insertions(+), 46 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 873b67b..98fb5be 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -278,8 +278,9 @@ Known local toolchain state: invalid channel counts rejected by `Texture2D::create(Image)`, renderer API texture-format to OpenGL internal/pixel/component token mapping including depth-stencil formats, RGBA8/RGBA32F - readback formats, checked byte-count math, and PBO pixel-buffer target/usage/access - mapping used by `RTT` and `PBO` readbacks, and framebuffer status naming + readback formats, checked byte-count math, PBO pixel-buffer target/usage/access + mapping, and PBO allocation/readback/map/unmap/delete dispatch sequences used + by retained `PBO` recording readbacks, and framebuffer status naming used by `RTT` and `Texture2D` diagnostics. It also owns the 2D texture target, framebuffer setup, readback format, mipmap target, and update component-type tokens used by `Texture2D`, plus cube-map binding and allocation face targets @@ -541,11 +542,13 @@ Known local toolchain state: - `pano_cli plan-recording-session` exposes `pp_app_core` recording start, stop, clear, platform cleanup, frame-count reset, and export progress-total planning as JSON; the live recording controls consume those contracts before - reaching legacy recording threads, PBO readback, and MP4 encoder execution. + reaching legacy recording threads, retained PBO readback call sites, and MP4 + encoder execution. - `src/legacy_recording_services.*` is the current app-shell bridge for recording start/stop/clear and MP4 export execution. It keeps those live paths - on the `pp_app_core` contracts while legacy recording thread ownership, PBO - readback, progress UI, platform cleanup, and `MP4Encoder` execution remain + on the `pp_app_core` contracts while legacy recording thread ownership, + retained PBO readback call sites, progress UI, platform cleanup, and + `MP4Encoder` execution remain tracked by `DEBT-0037`. - `pano_cli plan-share-file` exposes `pp_app_core` share availability planning as JSON for unsaved and saved document paths; the live platform share command @@ -623,8 +626,9 @@ Known local toolchain state: consumed by the retained `gl_state` utility, tested texture lifecycle/readback dispatch consumed by the retained `Texture2D` utility, tested framebuffer blit/readback dispatch consumed by retained `RTT` resize/copy/readback and RGBA8 region-readback - paths, tested framebuffer-to-texture 2D copy dispatch consumed by retained - canvas/UI paint paths, tested framebuffer + paths, tested pixel-buffer allocation/readback/map/unmap/delete dispatch + consumed by retained `PBO` recording readbacks, tested framebuffer-to-texture + 2D copy dispatch consumed by retained canvas/UI paint paths, tested framebuffer bind/restore dispatch consumed by retained `RTT` render-target pass entry and exit paths, tested depth renderbuffer allocation/delete and framebuffer depth attach/detach dispatch consumed by canvas object-drawing helpers, diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index e1421c9..7561902 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -54,7 +54,7 @@ agent or engineer to remove them without reconstructing context from chat. | 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::query_opengl_capability_detection`, `detect_opengl_feature_state`, and `render_device_features` as the backend conversion point; that feature snapshot now includes float32-linear filtering, so canvas stroke texture format selection, renderer diagnostics, grid lightmap render planning, and grid bake target selection no longer read `ShaderManager::ext_*` flags directly. `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. The retained `copy_framebuffer_to_texture_2d` utility bridge now routes 2D framebuffer-to-texture copies through tested `pp_renderer_gl` dispatch, but actual live stroke rasterization, dual-brush compositing, pattern feedback math, thumbnail layer compositing, brush-preview compositing, the retained cube-map framebuffer copy, and the retained `ShaderManager::ext_*` compatibility fields 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.*`, and retained `PBO` allocation/readback/map/unmap/delete operations now route through tested `pp_renderer_gl` dispatch, but the bridge still owns legacy recording thread startup/shutdown, platform recorded-file cleanup, progress UI, retained `App::rec_loop` readback call sites, 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`; `pp_renderer_gl_capabilities_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 scheduling, 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 | | DEBT-0040 | Open | Modernization | Close request, document save, and save-before-workflow planning/execution dispatch now consume pure `pp_app_core` through `App::request_close`, `App::save_document`, `App::continue_document_workflow_after_optional_save`, `pano_cli simulate-app-session`, `DocumentSaveServices`, `CloseRequestServices`, `DocumentWorkflowServices`, and `src/legacy_document_session_services.*`; Save dialog working-directory picker visibility/path formatting now dispatches through `PlatformServices`, but the bridge still opens legacy message boxes/save dialogs, calls `Canvas::I->project_save`, mutates the unsaved flag on close confirmation, invokes native app close, and routes save-version through the retained legacy dialog | Preserve current close/save/dirty-workflow behavior while document session execution moves toward app/document/UI/platform services | `pp_app_core_document_session_tests`; `pp_platform_api_tests`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `ctest --preset desktop-fast --build-config Debug` | Close prompt execution, native close requests, dirty-workflow save prompts, existing-project saves, save dialogs, save-version execution, and unsaved-flag mutation are owned by injected app/document/UI/platform services with `App` methods acting only as adapters | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 325d7b9..8823db3 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -814,6 +814,10 @@ Legacy `RTT` also exposes an RGBA8 region-readback helper that uses the same backend framebuffer readback dispatch; canvas pick/history/snapshot and transform history paths now call that helper instead of binding an RTT and calling `glReadPixels` directly. +Retained `PBO` recording readbacks now route pixel-buffer allocation, +framebuffer readback, map, unmap, and deletion through tested +`pp_renderer_gl` dispatch helpers; recording thread ownership, progress UI, and +MP4 execution remain tracked by DEBT-0037. Legacy `RTT::bindFramebuffer` and `RTT::unbindFramebuffer` now use tested `pp_renderer_gl` draw/read framebuffer binding snapshot and restore contracts, moving render-target pass entry/exit state management behind the backend. @@ -995,9 +999,10 @@ for future backend texture objects. `Texture2D` 2D texture binding, upload, mipmap generation, framebuffer readback setup, and update component-type tokens now delegate to `pp_renderer_gl`. `TextureCube` cube-map binding, allocation face targets, RGBA allocation format, and unsigned-byte component type also -delegate to `pp_renderer_gl`. RGBA8/RGBA32F readback formats, checked byte-count math, and PBO -pixel-buffer target/usage/access tokens used by `RTT` and `PBO` readbacks now -live in `pp_renderer_gl`. The framebuffer blit color mask and linear/nearest +delegate to `pp_renderer_gl`. RGBA8/RGBA32F readback formats, checked byte-count math, PBO +pixel-buffer target/usage/access tokens, and PBO allocation/readback/map/unmap/delete +dispatch sequences used by retained recording readbacks now live in +`pp_renderer_gl`. The framebuffer blit color mask and linear/nearest filter tokens used by `RTT::resize` and `RTT::copy`, renderer API blit-filter to OpenGL token mapping, plus the default render-target texture parameters, texture/renderbuffer targets, depth format, diff --git a/src/renderer_gl/opengl_capabilities.cpp b/src/renderer_gl/opengl_capabilities.cpp index 04ee771..46c98ca 100644 --- a/src/renderer_gl/opengl_capabilities.cpp +++ b/src/renderer_gl/opengl_capabilities.cpp @@ -969,6 +969,147 @@ pp::foundation::Status restore_opengl_framebuffer_binding( return pp::foundation::Status::success(); } +pp::foundation::Result allocate_opengl_pixel_buffer( + OpenGlPixelBufferAllocationDispatch dispatch) noexcept +{ + if (dispatch.gen_buffers == nullptr) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("OpenGL pixel-buffer allocation dispatch callback must not be null")); + } + + std::uint32_t buffer_id = 0U; + dispatch.gen_buffers(1U, &buffer_id); + if (buffer_id == 0U) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("OpenGL pixel-buffer allocation returned id 0")); + } + + return pp::foundation::Result::success(buffer_id); +} + +pp::foundation::Result readback_opengl_framebuffer_to_pixel_buffer( + std::int32_t width, + std::int32_t height, + OpenGlReadbackFormat format, + OpenGlPixelBufferReadbackDispatch dispatch) noexcept +{ + if (dispatch.gen_buffers == nullptr + || dispatch.bind_buffer == nullptr + || dispatch.buffer_data == nullptr + || dispatch.read_pixels == nullptr) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("OpenGL pixel-buffer readback dispatch callbacks must not be null")); + } + + if (width <= 0 + || height <= 0 + || format.pixel_format == 0U + || format.component_type == 0U + || format.bytes_per_pixel == 0U) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("OpenGL pixel-buffer readback parameters are invalid")); + } + + const auto buffer_id = allocate_opengl_pixel_buffer(OpenGlPixelBufferAllocationDispatch { + .gen_buffers = dispatch.gen_buffers, + }); + if (!buffer_id.ok()) { + return buffer_id; + } + + const auto target = pixel_pack_buffer_target(); + dispatch.bind_buffer(target, buffer_id.value()); + dispatch.buffer_data( + target, + static_cast(readback_byte_count( + format, + static_cast(width), + static_cast(height))), + nullptr, + pixel_buffer_stream_read_usage()); + dispatch.read_pixels(0, 0, width, height, format.pixel_format, format.component_type, nullptr); + dispatch.bind_buffer(target, 0U); + + return pp::foundation::Result::success(buffer_id.value()); +} + +pp::foundation::Result map_opengl_pixel_buffer( + std::uint32_t buffer_id, + std::int32_t width, + std::int32_t height, + OpenGlReadbackFormat format, + OpenGlPixelBufferMapDispatch dispatch) noexcept +{ + if (dispatch.bind_buffer == nullptr || dispatch.map_buffer_range == nullptr) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("OpenGL pixel-buffer map dispatch callbacks must not be null")); + } + + if (buffer_id == 0U + || width <= 0 + || height <= 0 + || format.pixel_format == 0U + || format.component_type == 0U + || format.bytes_per_pixel == 0U) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("OpenGL pixel-buffer map parameters are invalid")); + } + + const auto target = pixel_pack_buffer_target(); + dispatch.bind_buffer(target, buffer_id); + void* const mapped = dispatch.map_buffer_range( + target, + 0, + static_cast(readback_byte_count( + format, + static_cast(width), + static_cast(height))), + pixel_buffer_map_read_access()); + dispatch.bind_buffer(target, 0U); + + if (mapped == nullptr) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("OpenGL pixel-buffer map returned null")); + } + + return pp::foundation::Result::success(mapped); +} + +pp::foundation::Status unmap_opengl_pixel_buffer( + std::uint32_t buffer_id, + OpenGlPixelBufferUnmapDispatch dispatch) noexcept +{ + if (dispatch.bind_buffer == nullptr || dispatch.unmap_buffer == nullptr) { + return pp::foundation::Status::invalid_argument("OpenGL pixel-buffer unmap dispatch callbacks must not be null"); + } + + if (buffer_id == 0U) { + return pp::foundation::Status::invalid_argument("OpenGL pixel-buffer unmap buffer id is invalid"); + } + + const auto target = pixel_pack_buffer_target(); + dispatch.bind_buffer(target, buffer_id); + dispatch.unmap_buffer(target); + dispatch.bind_buffer(target, 0U); + return pp::foundation::Status::success(); +} + +pp::foundation::Status delete_opengl_pixel_buffer( + std::uint32_t buffer_id, + OpenGlPixelBufferDeleteDispatch dispatch) noexcept +{ + if (dispatch.delete_buffers == nullptr) { + return pp::foundation::Status::invalid_argument("OpenGL pixel-buffer delete dispatch callback must not be null"); + } + + if (buffer_id == 0U) { + return pp::foundation::Status::success(); + } + + dispatch.delete_buffers(1U, &buffer_id); + return pp::foundation::Status::success(); +} + pp::foundation::Result allocate_opengl_depth_renderbuffer( std::int32_t width, std::int32_t height, diff --git a/src/renderer_gl/opengl_capabilities.h b/src/renderer_gl/opengl_capabilities.h index 9a30dfd..93763a3 100644 --- a/src/renderer_gl/opengl_capabilities.h +++ b/src/renderer_gl/opengl_capabilities.h @@ -398,6 +398,12 @@ using OpenGlBufferDataFn = void (*)( std::intptr_t byte_count, const void* data, std::uint32_t usage) noexcept; +using OpenGlMapBufferRangeFn = void* (*)( + std::uint32_t target, + std::intptr_t offset, + std::intptr_t byte_count, + std::uint32_t access) noexcept; +using OpenGlUnmapBufferFn = void (*)(std::uint32_t target) noexcept; using OpenGlBindVertexArrayFn = void (*)(std::uint32_t vertex_array) noexcept; using OpenGlEnableVertexAttribArrayFn = void (*)(std::uint32_t index) noexcept; using OpenGlVertexAttribPointerFn = void (*)( @@ -642,6 +648,31 @@ struct OpenGlFramebufferRestoreDispatch { OpenGlBindFramebufferFn bind_framebuffer = nullptr; }; +struct OpenGlPixelBufferAllocationDispatch { + OpenGlGenObjectsFn gen_buffers = nullptr; +}; + +struct OpenGlPixelBufferReadbackDispatch { + OpenGlGenObjectsFn gen_buffers = nullptr; + OpenGlBindBufferFn bind_buffer = nullptr; + OpenGlBufferDataFn buffer_data = nullptr; + OpenGlReadPixelsFn read_pixels = nullptr; +}; + +struct OpenGlPixelBufferMapDispatch { + OpenGlBindBufferFn bind_buffer = nullptr; + OpenGlMapBufferRangeFn map_buffer_range = nullptr; +}; + +struct OpenGlPixelBufferUnmapDispatch { + OpenGlBindBufferFn bind_buffer = nullptr; + OpenGlUnmapBufferFn unmap_buffer = nullptr; +}; + +struct OpenGlPixelBufferDeleteDispatch { + OpenGlDeleteObjectsFn delete_buffers = nullptr; +}; + struct OpenGlDepthRenderbufferAllocationDispatch { OpenGlGenObjectsFn gen_renderbuffers = nullptr; OpenGlBindRenderbufferFn bind_renderbuffer = nullptr; @@ -864,6 +895,25 @@ struct OpenGlMeshDeleteDispatch { [[nodiscard]] pp::foundation::Status restore_opengl_framebuffer_binding( OpenGlFramebufferBindingState binding, OpenGlFramebufferRestoreDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Result allocate_opengl_pixel_buffer( + OpenGlPixelBufferAllocationDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Result readback_opengl_framebuffer_to_pixel_buffer( + std::int32_t width, + std::int32_t height, + OpenGlReadbackFormat format, + OpenGlPixelBufferReadbackDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Result map_opengl_pixel_buffer( + std::uint32_t buffer_id, + std::int32_t width, + std::int32_t height, + OpenGlReadbackFormat format, + OpenGlPixelBufferMapDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Status unmap_opengl_pixel_buffer( + std::uint32_t buffer_id, + OpenGlPixelBufferUnmapDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Status delete_opengl_pixel_buffer( + std::uint32_t buffer_id, + OpenGlPixelBufferDeleteDispatch dispatch) noexcept; [[nodiscard]] pp::foundation::Result allocate_opengl_depth_renderbuffer( std::int32_t width, std::int32_t height, diff --git a/src/rtt.cpp b/src/rtt.cpp index 417e698..c6484e1 100644 --- a/src/rtt.cpp +++ b/src/rtt.cpp @@ -78,6 +78,52 @@ void read_opengl_pixels( pixels); } +void gen_opengl_buffers(std::uint32_t count, std::uint32_t* ids) noexcept +{ + glGenBuffers(static_cast(count), reinterpret_cast(ids)); +} + +void delete_opengl_buffers(std::uint32_t count, const std::uint32_t* ids) noexcept +{ + glDeleteBuffers(static_cast(count), reinterpret_cast(ids)); +} + +void bind_opengl_buffer(std::uint32_t target, std::uint32_t buffer) noexcept +{ + glBindBuffer(static_cast(target), static_cast(buffer)); +} + +void set_opengl_buffer_data( + std::uint32_t target, + std::intptr_t byte_count, + const void* data, + std::uint32_t usage) noexcept +{ + glBufferData( + static_cast(target), + static_cast(byte_count), + data, + static_cast(usage)); +} + +void* map_opengl_buffer_range( + std::uint32_t target, + std::intptr_t offset, + std::intptr_t byte_count, + std::uint32_t access) noexcept +{ + return glMapBufferRange( + static_cast(target), + static_cast(offset), + static_cast(byte_count), + static_cast(access)); +} + +void unmap_opengl_buffer(std::uint32_t target) noexcept +{ + glUnmapBuffer(static_cast(target)); +} + } RTT& RTT::operator=(RTT&& other) @@ -673,7 +719,16 @@ PBO::~PBO() noexcept bool PBO::create() noexcept { App::I->render_task([this] { - glGenBuffers(1, &buffer_id); + const auto result = pp::renderer::gl::allocate_opengl_pixel_buffer( + pp::renderer::gl::OpenGlPixelBufferAllocationDispatch { + .gen_buffers = gen_opengl_buffers, + }); + if (!result.ok()) { + LOG("%s", result.status().message); + buffer_id = 0U; + return; + } + buffer_id = result.value(); }); return true; } @@ -683,29 +738,24 @@ bool PBO::create(RTT& rtt) noexcept App::I->render_task([this, &rtt] { width = rtt.getWidth(); height = rtt.getHeight(); - const auto readback = pp::renderer::gl::rgba8_readback_format(); - const auto buffer_target = static_cast(pp::renderer::gl::pixel_pack_buffer_target()); rtt.bindFramebuffer(); - glGenBuffers(1, &buffer_id); - glBindBuffer(buffer_target, buffer_id); - glBufferData( - buffer_target, - static_cast(pp::renderer::gl::readback_byte_count( - readback, - static_cast(width), - static_cast(height))), - 0, - static_cast(pp::renderer::gl::pixel_buffer_stream_read_usage())); - glReadPixels( - 0, - 0, + const auto result = pp::renderer::gl::readback_opengl_framebuffer_to_pixel_buffer( width, height, - static_cast(readback.pixel_format), - static_cast(readback.component_type), - 0); - glBindBuffer(buffer_target, 0); + pp::renderer::gl::rgba8_readback_format(), + pp::renderer::gl::OpenGlPixelBufferReadbackDispatch { + .gen_buffers = gen_opengl_buffers, + .bind_buffer = bind_opengl_buffer, + .buffer_data = set_opengl_buffer_data, + .read_pixels = read_opengl_pixels, + }); rtt.unbindFramebuffer(); + if (!result.ok()) { + LOG("%s", result.status().message); + buffer_id = 0U; + return; + } + buffer_id = result.value(); }); return true; } @@ -715,7 +765,14 @@ void PBO::destroy() noexcept if (buffer_id) { App::I->render_task_async([id=buffer_id] { - glDeleteBuffers(1, &id); + const auto status = pp::renderer::gl::delete_opengl_pixel_buffer( + id, + pp::renderer::gl::OpenGlPixelBufferDeleteDispatch { + .delete_buffers = delete_opengl_buffers, + }); + if (!status.ok()) { + LOG("%s", status.message); + } }); buffer_id = 0; bound_slot = 0; @@ -746,16 +803,21 @@ void PBO::unbind() noexcept glm::uint8_t* PBO::map() noexcept { App::I->render_task([this] { - const auto readback = pp::renderer::gl::rgba8_readback_format(); - const auto buffer_target = static_cast(pp::renderer::gl::pixel_pack_buffer_target()); - glBindBuffer(buffer_target, buffer_id); - mapped_ptr = (GLubyte*)glMapBufferRange(buffer_target, 0, - static_cast(pp::renderer::gl::readback_byte_count( - readback, - static_cast(width), - static_cast(height))), - static_cast(pp::renderer::gl::pixel_buffer_map_read_access())); - glBindBuffer(buffer_target, 0); + const auto result = pp::renderer::gl::map_opengl_pixel_buffer( + buffer_id, + width, + height, + pp::renderer::gl::rgba8_readback_format(), + pp::renderer::gl::OpenGlPixelBufferMapDispatch { + .bind_buffer = bind_opengl_buffer, + .map_buffer_range = map_opengl_buffer_range, + }); + if (!result.ok()) { + LOG("%s", result.status().message); + mapped_ptr = nullptr; + return; + } + mapped_ptr = static_cast(result.value()); }); return mapped_ptr; } @@ -763,9 +825,14 @@ glm::uint8_t* PBO::map() noexcept void PBO::unmap() noexcept { App::I->render_task([this] { - const auto buffer_target = static_cast(pp::renderer::gl::pixel_pack_buffer_target()); - glBindBuffer(buffer_target, buffer_id); - glUnmapBuffer(buffer_target); - glBindBuffer(buffer_target, 0); + const auto status = pp::renderer::gl::unmap_opengl_pixel_buffer( + buffer_id, + pp::renderer::gl::OpenGlPixelBufferUnmapDispatch { + .bind_buffer = bind_opengl_buffer, + .unmap_buffer = unmap_opengl_buffer, + }); + if (!status.ok()) { + LOG("%s", status.message); + } }); } diff --git a/tests/renderer_gl/capabilities_tests.cpp b/tests/renderer_gl/capabilities_tests.cpp index 2a924e9..b365cdc 100644 --- a/tests/renderer_gl/capabilities_tests.cpp +++ b/tests/renderer_gl/capabilities_tests.cpp @@ -166,6 +166,13 @@ struct RecordedOpenGlBufferDataCall { std::uint32_t usage = 0; }; +struct RecordedOpenGlBufferMapCall { + std::uint32_t target = 0; + std::intptr_t offset = 0; + std::intptr_t byte_count = 0; + std::uint32_t access = 0; +}; + struct RecordedOpenGlVertexAttribPointerCall { std::uint32_t index = 0; std::int32_t component_count = 0; @@ -237,6 +244,8 @@ std::vector recorded_generated_vertex_array_counts; std::vector recorded_deleted_vertex_arrays; std::vector recorded_buffer_bind_calls; std::vector recorded_buffer_data_calls; +std::vector recorded_buffer_map_calls; +std::vector recorded_buffer_unmap_calls; std::vector recorded_vertex_array_bind_calls; std::vector recorded_enabled_vertex_attributes; std::vector recorded_vertex_attrib_pointer_calls; @@ -1002,6 +1011,41 @@ void record_buffer_data( }); } +void* record_map_buffer_range( + std::uint32_t target, + std::intptr_t offset, + std::intptr_t byte_count, + std::uint32_t access) noexcept +{ + recorded_buffer_map_calls.push_back(RecordedOpenGlBufferMapCall { + .target = target, + .offset = offset, + .byte_count = byte_count, + .access = access, + }); + return reinterpret_cast(0x1234U); +} + +void* record_failed_map_buffer_range( + std::uint32_t target, + std::intptr_t offset, + std::intptr_t byte_count, + std::uint32_t access) noexcept +{ + recorded_buffer_map_calls.push_back(RecordedOpenGlBufferMapCall { + .target = target, + .offset = offset, + .byte_count = byte_count, + .access = access, + }); + return nullptr; +} + +void record_unmap_buffer(std::uint32_t target) noexcept +{ + recorded_buffer_unmap_calls.push_back(target); +} + void record_gen_vertex_arrays(std::uint32_t count, std::uint32_t* ids) noexcept { recorded_generated_vertex_array_counts.push_back(count); @@ -3779,6 +3823,170 @@ void rejects_invalid_mesh_dispatch(pp::tests::Harness& h) PP_EXPECT(h, missing_delete_dispatch.code == pp::foundation::StatusCode::invalid_argument); } +void creates_reads_maps_and_deletes_pixel_buffers_through_dispatch(pp::tests::Harness& h) +{ + recorded_generated_buffer_counts.clear(); + recorded_buffer_bind_calls.clear(); + recorded_buffer_data_calls.clear(); + recorded_read_pixels_calls.clear(); + recorded_buffer_map_calls.clear(); + recorded_buffer_unmap_calls.clear(); + recorded_deleted_buffers.clear(); + next_buffer_id = 801U; + + const auto allocated = pp::renderer::gl::allocate_opengl_pixel_buffer( + pp::renderer::gl::OpenGlPixelBufferAllocationDispatch { + .gen_buffers = record_gen_buffers, + }); + const auto readback = pp::renderer::gl::readback_opengl_framebuffer_to_pixel_buffer( + 8, + 4, + pp::renderer::gl::rgba8_readback_format(), + pp::renderer::gl::OpenGlPixelBufferReadbackDispatch { + .gen_buffers = record_gen_buffers, + .bind_buffer = record_bind_buffer, + .buffer_data = record_buffer_data, + .read_pixels = record_read_pixels, + }); + const auto mapped = pp::renderer::gl::map_opengl_pixel_buffer( + readback.ok() ? readback.value() : 0U, + 8, + 4, + pp::renderer::gl::rgba8_readback_format(), + pp::renderer::gl::OpenGlPixelBufferMapDispatch { + .bind_buffer = record_bind_buffer, + .map_buffer_range = record_map_buffer_range, + }); + const auto unmap_status = pp::renderer::gl::unmap_opengl_pixel_buffer( + readback.ok() ? readback.value() : 0U, + pp::renderer::gl::OpenGlPixelBufferUnmapDispatch { + .bind_buffer = record_bind_buffer, + .unmap_buffer = record_unmap_buffer, + }); + const auto delete_status = pp::renderer::gl::delete_opengl_pixel_buffer( + readback.ok() ? readback.value() : 0U, + pp::renderer::gl::OpenGlPixelBufferDeleteDispatch { + .delete_buffers = record_delete_buffers, + }); + + PP_EXPECT(h, allocated.ok()); + PP_EXPECT(h, allocated.value() == 801U); + PP_EXPECT(h, readback.ok()); + PP_EXPECT(h, readback.value() == 802U); + PP_EXPECT(h, mapped.ok()); + PP_EXPECT(h, mapped.value() == reinterpret_cast(0x1234U)); + PP_EXPECT(h, unmap_status.ok()); + PP_EXPECT(h, delete_status.ok()); + PP_EXPECT(h, recorded_generated_buffer_counts.size() == 2U); + PP_EXPECT(h, recorded_generated_buffer_counts[0] == 1U); + PP_EXPECT(h, recorded_generated_buffer_counts[1] == 1U); + PP_EXPECT(h, recorded_buffer_bind_calls.size() == 6U); + PP_EXPECT(h, recorded_buffer_bind_calls[0].target == 0x88EBU); + PP_EXPECT(h, recorded_buffer_bind_calls[0].buffer == 802U); + PP_EXPECT(h, recorded_buffer_bind_calls[1].target == 0x88EBU); + PP_EXPECT(h, recorded_buffer_bind_calls[1].buffer == 0U); + PP_EXPECT(h, recorded_buffer_bind_calls[2].buffer == 802U); + PP_EXPECT(h, recorded_buffer_bind_calls[3].buffer == 0U); + PP_EXPECT(h, recorded_buffer_bind_calls[4].buffer == 802U); + PP_EXPECT(h, recorded_buffer_bind_calls[5].buffer == 0U); + PP_EXPECT(h, recorded_buffer_data_calls.size() == 1U); + PP_EXPECT(h, recorded_buffer_data_calls[0].target == 0x88EBU); + PP_EXPECT(h, recorded_buffer_data_calls[0].byte_count == 128); + PP_EXPECT(h, recorded_buffer_data_calls[0].data == nullptr); + PP_EXPECT(h, recorded_buffer_data_calls[0].usage == 0x88E1U); + PP_EXPECT(h, recorded_read_pixels_calls.size() == 1U); + PP_EXPECT(h, recorded_read_pixels_calls[0].width == 8); + PP_EXPECT(h, recorded_read_pixels_calls[0].height == 4); + PP_EXPECT(h, recorded_read_pixels_calls[0].pixel_format == 0x1908U); + PP_EXPECT(h, recorded_read_pixels_calls[0].component_type == 0x1401U); + PP_EXPECT(h, recorded_read_pixels_calls[0].pixels == nullptr); + PP_EXPECT(h, recorded_buffer_map_calls.size() == 1U); + PP_EXPECT(h, recorded_buffer_map_calls[0].target == 0x88EBU); + PP_EXPECT(h, recorded_buffer_map_calls[0].offset == 0); + PP_EXPECT(h, recorded_buffer_map_calls[0].byte_count == 128); + PP_EXPECT(h, recorded_buffer_map_calls[0].access == 0x0001U); + PP_EXPECT(h, recorded_buffer_unmap_calls.size() == 1U); + PP_EXPECT(h, recorded_buffer_unmap_calls[0] == 0x88EBU); + PP_EXPECT(h, recorded_deleted_buffers.size() == 1U); + PP_EXPECT(h, recorded_deleted_buffers[0] == 802U); +} + +void rejects_invalid_pixel_buffer_dispatch(pp::tests::Harness& h) +{ + const auto missing_allocate_dispatch = pp::renderer::gl::allocate_opengl_pixel_buffer( + pp::renderer::gl::OpenGlPixelBufferAllocationDispatch {}); + const auto missing_readback_dispatch = pp::renderer::gl::readback_opengl_framebuffer_to_pixel_buffer( + 8, + 4, + pp::renderer::gl::rgba8_readback_format(), + pp::renderer::gl::OpenGlPixelBufferReadbackDispatch {}); + const auto invalid_readback_size = pp::renderer::gl::readback_opengl_framebuffer_to_pixel_buffer( + 0, + 4, + pp::renderer::gl::rgba8_readback_format(), + pp::renderer::gl::OpenGlPixelBufferReadbackDispatch { + .gen_buffers = record_gen_buffers, + .bind_buffer = record_bind_buffer, + .buffer_data = record_buffer_data, + .read_pixels = record_read_pixels, + }); + const auto missing_map_dispatch = pp::renderer::gl::map_opengl_pixel_buffer( + 1U, + 8, + 4, + pp::renderer::gl::rgba8_readback_format(), + pp::renderer::gl::OpenGlPixelBufferMapDispatch {}); + const auto invalid_map_buffer = pp::renderer::gl::map_opengl_pixel_buffer( + 0U, + 8, + 4, + pp::renderer::gl::rgba8_readback_format(), + pp::renderer::gl::OpenGlPixelBufferMapDispatch { + .bind_buffer = record_bind_buffer, + .map_buffer_range = record_map_buffer_range, + }); + const auto failed_map = pp::renderer::gl::map_opengl_pixel_buffer( + 1U, + 8, + 4, + pp::renderer::gl::rgba8_readback_format(), + pp::renderer::gl::OpenGlPixelBufferMapDispatch { + .bind_buffer = record_bind_buffer, + .map_buffer_range = record_failed_map_buffer_range, + }); + const auto missing_unmap_dispatch = pp::renderer::gl::unmap_opengl_pixel_buffer( + 1U, + pp::renderer::gl::OpenGlPixelBufferUnmapDispatch {}); + const auto invalid_unmap_buffer = pp::renderer::gl::unmap_opengl_pixel_buffer( + 0U, + pp::renderer::gl::OpenGlPixelBufferUnmapDispatch { + .bind_buffer = record_bind_buffer, + .unmap_buffer = record_unmap_buffer, + }); + const auto missing_delete_dispatch = pp::renderer::gl::delete_opengl_pixel_buffer( + 1U, + pp::renderer::gl::OpenGlPixelBufferDeleteDispatch {}); + + PP_EXPECT(h, !missing_allocate_dispatch.ok()); + PP_EXPECT(h, missing_allocate_dispatch.status().code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(h, !missing_readback_dispatch.ok()); + PP_EXPECT(h, missing_readback_dispatch.status().code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(h, !invalid_readback_size.ok()); + PP_EXPECT(h, invalid_readback_size.status().code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(h, !missing_map_dispatch.ok()); + PP_EXPECT(h, missing_map_dispatch.status().code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(h, !invalid_map_buffer.ok()); + PP_EXPECT(h, invalid_map_buffer.status().code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(h, !failed_map.ok()); + PP_EXPECT(h, failed_map.status().code == pp::foundation::StatusCode::out_of_range); + PP_EXPECT(h, !missing_unmap_dispatch.ok()); + PP_EXPECT(h, missing_unmap_dispatch.code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(h, !invalid_unmap_buffer.ok()); + PP_EXPECT(h, invalid_unmap_buffer.code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(h, !missing_delete_dispatch.ok()); + PP_EXPECT(h, missing_delete_dispatch.code == pp::foundation::StatusCode::invalid_argument); +} + void updates_texture_2d_through_dispatch(pp::tests::Harness& h) { recorded_binding_calls.clear(); @@ -4759,6 +4967,8 @@ int main() harness.run("creates_single_vertex_array_mesh_with_deferred_upload", creates_single_vertex_array_mesh_with_deferred_upload); harness.run("updates_draws_and_deletes_mesh_through_dispatch", updates_draws_and_deletes_mesh_through_dispatch); harness.run("rejects_invalid_mesh_dispatch", rejects_invalid_mesh_dispatch); + harness.run("creates_reads_maps_and_deletes_pixel_buffers_through_dispatch", creates_reads_maps_and_deletes_pixel_buffers_through_dispatch); + harness.run("rejects_invalid_pixel_buffer_dispatch", rejects_invalid_pixel_buffer_dispatch); harness.run("updates_texture_2d_through_dispatch", updates_texture_2d_through_dispatch); harness.run("copies_framebuffer_to_texture_2d_through_dispatch", copies_framebuffer_to_texture_2d_through_dispatch); harness.run("skips_zero_sized_framebuffer_to_texture_copies", skips_zero_sized_framebuffer_to_texture_copies);