Route PBO readbacks through GL backend

This commit is contained in:
2026-06-04 21:29:49 +02:00
parent 45802dfc7c
commit fc20851462
7 changed files with 523 additions and 46 deletions

View File

@@ -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,

View File

@@ -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 |

View File

@@ -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,

View File

@@ -969,6 +969,147 @@ pp::foundation::Status restore_opengl_framebuffer_binding(
return pp::foundation::Status::success();
}
pp::foundation::Result<std::uint32_t> allocate_opengl_pixel_buffer(
OpenGlPixelBufferAllocationDispatch dispatch) noexcept
{
if (dispatch.gen_buffers == nullptr) {
return pp::foundation::Result<std::uint32_t>::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<std::uint32_t>::failure(
pp::foundation::Status::out_of_range("OpenGL pixel-buffer allocation returned id 0"));
}
return pp::foundation::Result<std::uint32_t>::success(buffer_id);
}
pp::foundation::Result<std::uint32_t> 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<std::uint32_t>::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<std::uint32_t>::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<std::intptr_t>(readback_byte_count(
format,
static_cast<std::uint32_t>(width),
static_cast<std::uint32_t>(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<std::uint32_t>::success(buffer_id.value());
}
pp::foundation::Result<void*> 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<void*>::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<void*>::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<std::intptr_t>(readback_byte_count(
format,
static_cast<std::uint32_t>(width),
static_cast<std::uint32_t>(height))),
pixel_buffer_map_read_access());
dispatch.bind_buffer(target, 0U);
if (mapped == nullptr) {
return pp::foundation::Result<void*>::failure(
pp::foundation::Status::out_of_range("OpenGL pixel-buffer map returned null"));
}
return pp::foundation::Result<void*>::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<std::uint32_t> allocate_opengl_depth_renderbuffer(
std::int32_t width,
std::int32_t height,

View File

@@ -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<std::uint32_t> allocate_opengl_pixel_buffer(
OpenGlPixelBufferAllocationDispatch dispatch) noexcept;
[[nodiscard]] pp::foundation::Result<std::uint32_t> readback_opengl_framebuffer_to_pixel_buffer(
std::int32_t width,
std::int32_t height,
OpenGlReadbackFormat format,
OpenGlPixelBufferReadbackDispatch dispatch) noexcept;
[[nodiscard]] pp::foundation::Result<void*> 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<std::uint32_t> allocate_opengl_depth_renderbuffer(
std::int32_t width,
std::int32_t height,

View File

@@ -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<GLsizei>(count), reinterpret_cast<GLuint*>(ids));
}
void delete_opengl_buffers(std::uint32_t count, const std::uint32_t* ids) noexcept
{
glDeleteBuffers(static_cast<GLsizei>(count), reinterpret_cast<const GLuint*>(ids));
}
void bind_opengl_buffer(std::uint32_t target, std::uint32_t buffer) noexcept
{
glBindBuffer(static_cast<GLenum>(target), static_cast<GLuint>(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<GLenum>(target),
static_cast<GLsizeiptr>(byte_count),
data,
static_cast<GLenum>(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<GLenum>(target),
static_cast<GLintptr>(offset),
static_cast<GLsizeiptr>(byte_count),
static_cast<GLbitfield>(access));
}
void unmap_opengl_buffer(std::uint32_t target) noexcept
{
glUnmapBuffer(static_cast<GLenum>(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<GLenum>(pp::renderer::gl::pixel_pack_buffer_target());
rtt.bindFramebuffer();
glGenBuffers(1, &buffer_id);
glBindBuffer(buffer_target, buffer_id);
glBufferData(
buffer_target,
static_cast<GLsizeiptr>(pp::renderer::gl::readback_byte_count(
readback,
static_cast<std::uint32_t>(width),
static_cast<std::uint32_t>(height))),
0,
static_cast<GLenum>(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<GLenum>(readback.pixel_format),
static_cast<GLenum>(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<GLenum>(pp::renderer::gl::pixel_pack_buffer_target());
glBindBuffer(buffer_target, buffer_id);
mapped_ptr = (GLubyte*)glMapBufferRange(buffer_target, 0,
static_cast<GLsizeiptr>(pp::renderer::gl::readback_byte_count(
readback,
static_cast<std::uint32_t>(width),
static_cast<std::uint32_t>(height))),
static_cast<GLbitfield>(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<glm::uint8_t*>(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<GLenum>(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);
}
});
}

View File

@@ -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<std::uint32_t> recorded_generated_vertex_array_counts;
std::vector<std::uint32_t> recorded_deleted_vertex_arrays;
std::vector<RecordedOpenGlBufferBindCall> recorded_buffer_bind_calls;
std::vector<RecordedOpenGlBufferDataCall> recorded_buffer_data_calls;
std::vector<RecordedOpenGlBufferMapCall> recorded_buffer_map_calls;
std::vector<std::uint32_t> recorded_buffer_unmap_calls;
std::vector<std::uint32_t> recorded_vertex_array_bind_calls;
std::vector<std::uint32_t> recorded_enabled_vertex_attributes;
std::vector<RecordedOpenGlVertexAttribPointerCall> 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<void*>(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<void*>(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);