From ae69f7437f7d2731314cd19929a4f16b3e876f15 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Wed, 3 Jun 2026 06:24:56 +0200 Subject: [PATCH] Route Texture2D through renderer GL --- docs/modernization/build-inventory.md | 5 +- docs/modernization/roadmap.md | 4 + src/renderer_gl/opengl_capabilities.cpp | 186 ++++++++++ src/renderer_gl/opengl_capabilities.h | 122 +++++++ src/texture.cpp | 277 +++++++++++---- tests/renderer_gl/capabilities_tests.cpp | 434 +++++++++++++++++++++++ 6 files changed, 959 insertions(+), 69 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 850bdea..9c729fc 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -466,8 +466,9 @@ Known local toolchain state: viewport/scissor dispatch consumed by `App::draw` and `App::vr_draw_ui`, tested generic capability/buffer-clear dispatch consumed by VR draw state setup, tested saved-state snapshot/restore dispatch consumed by the retained - `gl_state` utility, plus renderer API to OpenGL token mapping and - command-planning contracts used by the OpenGL parity work. + `gl_state` utility, tested texture lifecycle/readback dispatch consumed by + the retained `Texture2D` utility, 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 c93abad..b7b6a7b 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -540,6 +540,10 @@ tested `pp_renderer_gl` saved-state dispatch contracts, covering capability state, viewport, clear color, framebuffer/program bindings, active texture, 2D texture slots, samplers, and cube-map binding without changing the legacy utility's public fields. +Legacy `Texture2D` allocation, binding, deletion, mipmap generation, region +update, and framebuffer readback now execute through tested `pp_renderer_gl` +texture dispatch contracts. This keeps the app API stable while moving another +resource lifecycle path behind the renderer backend boundary. 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/renderer_gl/opengl_capabilities.cpp b/src/renderer_gl/opengl_capabilities.cpp index dcbe184..f1bbfb9 100644 --- a/src/renderer_gl/opengl_capabilities.cpp +++ b/src/renderer_gl/opengl_capabilities.cpp @@ -422,6 +422,192 @@ pp::foundation::Status clear_opengl_buffers( return pp::foundation::Status::success(); } +pp::foundation::Result allocate_opengl_texture_2d( + OpenGlTexture2DAllocation allocation, + OpenGlTexture2DAllocationDispatch dispatch) noexcept +{ + if (dispatch.gen_textures == nullptr + || dispatch.bind_texture == nullptr + || dispatch.tex_image_2d == nullptr) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("OpenGL texture allocation dispatch callbacks must not be null")); + } + + if (allocation.width <= 0 + || allocation.height <= 0 + || allocation.internal_format == 0 + || allocation.pixel_format == 0U + || allocation.component_type == 0U) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("OpenGL texture allocation parameters are invalid")); + } + + std::uint32_t texture_id = 0U; + dispatch.gen_textures(1U, &texture_id); + if (texture_id == 0U) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("OpenGL texture allocation returned id 0")); + } + + dispatch.bind_texture(texture_2d_target(), texture_id); + dispatch.tex_image_2d( + texture_2d_target(), + 0, + allocation.internal_format, + allocation.width, + allocation.height, + 0, + allocation.pixel_format, + allocation.component_type, + allocation.data); + dispatch.bind_texture(texture_2d_target(), default_framebuffer_id()); + return pp::foundation::Result::success(texture_id); +} + +pp::foundation::Status delete_opengl_texture_2d( + std::uint32_t texture_id, + OpenGlTexture2DDeleteDispatch dispatch) noexcept +{ + if (dispatch.delete_textures == nullptr) { + return pp::foundation::Status::invalid_argument("OpenGL texture delete dispatch callback must not be null"); + } + + if (texture_id == 0U) { + return pp::foundation::Status::success(); + } + + dispatch.delete_textures(1U, &texture_id); + return pp::foundation::Status::success(); +} + +pp::foundation::Status bind_opengl_texture_2d( + std::uint32_t texture_id, + OpenGlTexture2DBindDispatch dispatch) noexcept +{ + if (dispatch.bind_texture == nullptr) { + return pp::foundation::Status::invalid_argument("OpenGL texture bind dispatch callback must not be null"); + } + + dispatch.bind_texture(texture_2d_target(), texture_id); + return pp::foundation::Status::success(); +} + +pp::foundation::Status update_opengl_texture_2d( + OpenGlTexture2DUpdate update, + OpenGlTexture2DUpdateDispatch dispatch) noexcept +{ + if (dispatch.bind_texture == nullptr || dispatch.tex_sub_image_2d == nullptr) { + return pp::foundation::Status::invalid_argument("OpenGL texture update dispatch callbacks must not be null"); + } + + if (update.texture_id == 0U + || update.width <= 0 + || update.height <= 0 + || update.pixel_format == 0U + || update.component_type == 0U + || update.data == nullptr) { + return pp::foundation::Status::invalid_argument("OpenGL texture update parameters are invalid"); + } + + dispatch.bind_texture(texture_2d_target(), update.texture_id); + dispatch.tex_sub_image_2d( + texture_2d_target(), + 0, + 0, + 0, + update.width, + update.height, + update.pixel_format, + update.component_type, + update.data); + return pp::foundation::Status::success(); +} + +pp::foundation::Status generate_opengl_texture_2d_mipmaps( + std::uint32_t texture_id, + OpenGlTexture2DMipmapDispatch dispatch) noexcept +{ + if (dispatch.bind_texture == nullptr || dispatch.generate_mipmap == nullptr) { + return pp::foundation::Status::invalid_argument("OpenGL texture mipmap dispatch callbacks must not be null"); + } + + if (texture_id == 0U) { + return pp::foundation::Status::invalid_argument("OpenGL texture mipmap target is invalid"); + } + + dispatch.bind_texture(texture_2d_target(), texture_id); + dispatch.generate_mipmap(texture_2d_target()); + dispatch.bind_texture(texture_2d_target(), default_framebuffer_id()); + return pp::foundation::Status::success(); +} + +pp::foundation::Result readback_opengl_texture_2d( + OpenGlTexture2DReadback readback, + OpenGlTexture2DReadbackDispatch dispatch) noexcept +{ + if (dispatch.bind_texture == nullptr + || dispatch.gen_framebuffers == nullptr + || dispatch.get_integer == nullptr + || dispatch.bind_framebuffer == nullptr + || dispatch.framebuffer_texture_2d == nullptr + || dispatch.check_framebuffer_status == nullptr + || dispatch.read_pixels == nullptr + || dispatch.delete_framebuffers == nullptr) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("OpenGL texture readback dispatch callbacks must not be null")); + } + + if (readback.texture_id == 0U + || readback.width <= 0 + || readback.height <= 0 + || readback.format.pixel_format == 0U + || readback.format.component_type == 0U + || readback.format.bytes_per_pixel == 0U + || readback.pixels == nullptr) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("OpenGL texture readback parameters are invalid")); + } + + std::uint32_t framebuffer_id = 0U; + dispatch.gen_framebuffers(1U, &framebuffer_id); + if (framebuffer_id == 0U) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("OpenGL framebuffer allocation returned id 0")); + } + + std::int32_t previous_framebuffer = 0; + dispatch.get_integer(draw_framebuffer_binding_query(), &previous_framebuffer); + dispatch.bind_texture(texture_2d_target(), readback.texture_id); + dispatch.bind_framebuffer(framebuffer_target(), framebuffer_id); + dispatch.framebuffer_texture_2d( + framebuffer_target(), + framebuffer_color_attachment(), + texture_2d_target(), + readback.texture_id, + 0); + + const auto framebuffer_status = dispatch.check_framebuffer_status(framebuffer_target()); + OpenGlTexture2DReadbackResult result { + .framebuffer_status = framebuffer_status, + .pixels_read = false, + }; + if (framebuffer_status == framebuffer_complete_status()) { + dispatch.read_pixels( + 0, + 0, + readback.width, + readback.height, + readback.format.pixel_format, + readback.format.component_type, + readback.pixels); + result.pixels_read = true; + } + + dispatch.bind_framebuffer(framebuffer_target(), static_cast(previous_framebuffer)); + dispatch.delete_framebuffers(1U, &framebuffer_id); + return pp::foundation::Result::success(result); +} + 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 e3f318b..c74fe62 100644 --- a/src/renderer_gl/opengl_capabilities.h +++ b/src/renderer_gl/opengl_capabilities.h @@ -110,6 +110,37 @@ struct OpenGlRendererTextureFormat { std::uint32_t bytes_per_pixel = 0; }; +struct OpenGlTexture2DAllocation { + std::int32_t width = 0; + std::int32_t height = 0; + std::int32_t internal_format = 0; + std::uint32_t pixel_format = 0; + std::uint32_t component_type = 0; + const void* data = nullptr; +}; + +struct OpenGlTexture2DUpdate { + std::uint32_t texture_id = 0; + std::int32_t width = 0; + std::int32_t height = 0; + std::uint32_t pixel_format = 0; + std::uint32_t component_type = 0; + const void* data = nullptr; +}; + +struct OpenGlTexture2DReadback { + std::uint32_t texture_id = 0; + std::int32_t width = 0; + std::int32_t height = 0; + OpenGlReadbackFormat format; + void* pixels = nullptr; +}; + +struct OpenGlTexture2DReadbackResult { + std::uint32_t framebuffer_status = 0; + bool pixels_read = false; +}; + struct OpenGlWindowsWglContextConfig { std::array context_attributes {}; std::array pixel_format_attributes {}; @@ -154,6 +185,44 @@ using OpenGlUseProgramFn = void (*)(std::uint32_t program) noexcept; using OpenGlBindFramebufferFn = void (*)(std::uint32_t target, std::uint32_t framebuffer) noexcept; using OpenGlBindTextureFn = void (*)(std::uint32_t target, std::uint32_t texture) noexcept; using OpenGlBindSamplerFn = void (*)(std::uint32_t unit, std::uint32_t sampler) noexcept; +using OpenGlGenObjectsFn = void (*)(std::uint32_t count, std::uint32_t* ids) noexcept; +using OpenGlDeleteObjectsFn = void (*)(std::uint32_t count, const std::uint32_t* ids) noexcept; +using OpenGlTexImage2DFn = void (*)( + std::uint32_t target, + std::int32_t level, + std::int32_t internal_format, + std::int32_t width, + std::int32_t height, + std::int32_t border, + std::uint32_t pixel_format, + std::uint32_t component_type, + const void* data) noexcept; +using OpenGlTexSubImage2DFn = void (*)( + std::uint32_t target, + std::int32_t level, + std::int32_t x, + std::int32_t y, + std::int32_t width, + std::int32_t height, + std::uint32_t pixel_format, + std::uint32_t component_type, + const void* data) noexcept; +using OpenGlGenerateMipmapFn = void (*)(std::uint32_t target) noexcept; +using OpenGlFramebufferTexture2DFn = void (*)( + std::uint32_t target, + std::uint32_t attachment, + std::uint32_t texture_target, + std::uint32_t texture, + std::int32_t level) noexcept; +using OpenGlCheckFramebufferStatusFn = std::uint32_t (*)(std::uint32_t target) noexcept; +using OpenGlReadPixelsFn = void (*)( + std::int32_t x, + std::int32_t y, + std::int32_t width, + std::int32_t height, + std::uint32_t pixel_format, + std::uint32_t component_type, + void* pixels) noexcept; struct OpenGlStateDispatch { OpenGlCapabilityFn enable = nullptr; @@ -228,6 +297,41 @@ struct OpenGlBufferClearDispatch { OpenGlClearFn clear = nullptr; }; +struct OpenGlTexture2DAllocationDispatch { + OpenGlGenObjectsFn gen_textures = nullptr; + OpenGlBindTextureFn bind_texture = nullptr; + OpenGlTexImage2DFn tex_image_2d = nullptr; +}; + +struct OpenGlTexture2DDeleteDispatch { + OpenGlDeleteObjectsFn delete_textures = nullptr; +}; + +struct OpenGlTexture2DBindDispatch { + OpenGlBindTextureFn bind_texture = nullptr; +}; + +struct OpenGlTexture2DUpdateDispatch { + OpenGlBindTextureFn bind_texture = nullptr; + OpenGlTexSubImage2DFn tex_sub_image_2d = nullptr; +}; + +struct OpenGlTexture2DMipmapDispatch { + OpenGlBindTextureFn bind_texture = nullptr; + OpenGlGenerateMipmapFn generate_mipmap = nullptr; +}; + +struct OpenGlTexture2DReadbackDispatch { + OpenGlBindTextureFn bind_texture = nullptr; + OpenGlGenObjectsFn gen_framebuffers = nullptr; + OpenGlGetIntegerFn get_integer = nullptr; + OpenGlBindFramebufferFn bind_framebuffer = nullptr; + OpenGlFramebufferTexture2DFn framebuffer_texture_2d = nullptr; + OpenGlCheckFramebufferStatusFn check_framebuffer_status = nullptr; + OpenGlReadPixelsFn read_pixels = nullptr; + OpenGlDeleteObjectsFn delete_framebuffers = nullptr; +}; + [[nodiscard]] OpenGlCapabilities detect_opengl_capabilities( std::span extensions, OpenGlRuntime runtime) noexcept; @@ -260,6 +364,24 @@ struct OpenGlBufferClearDispatch { [[nodiscard]] pp::foundation::Status clear_opengl_buffers( std::uint32_t mask, OpenGlBufferClearDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Result allocate_opengl_texture_2d( + OpenGlTexture2DAllocation allocation, + OpenGlTexture2DAllocationDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Status delete_opengl_texture_2d( + std::uint32_t texture_id, + OpenGlTexture2DDeleteDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Status bind_opengl_texture_2d( + std::uint32_t texture_id, + OpenGlTexture2DBindDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Status update_opengl_texture_2d( + OpenGlTexture2DUpdate update, + OpenGlTexture2DUpdateDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Status generate_opengl_texture_2d_mipmaps( + std::uint32_t texture_id, + OpenGlTexture2DMipmapDispatch dispatch) noexcept; +[[nodiscard]] pp::foundation::Result readback_opengl_texture_2d( + OpenGlTexture2DReadback readback, + OpenGlTexture2DReadbackDispatch dispatch) noexcept; [[nodiscard]] std::uint32_t extension_count_query() noexcept; [[nodiscard]] std::uint32_t extension_string_name() noexcept; diff --git a/src/texture.cpp b/src/texture.cpp index 346fce8..f71aa9b 100644 --- a/src/texture.cpp +++ b/src/texture.cpp @@ -9,24 +9,134 @@ namespace { -[[nodiscard]] GLenum texture_2d_target() noexcept -{ - return static_cast(pp::renderer::gl::texture_2d_target()); -} - [[nodiscard]] GLenum texture_cube_map_target() noexcept { return static_cast(pp::renderer::gl::texture_cube_map_target()); } -[[nodiscard]] GLenum framebuffer_target() noexcept +void gen_opengl_textures(std::uint32_t count, std::uint32_t* ids) noexcept { - return static_cast(pp::renderer::gl::framebuffer_target()); + glGenTextures(static_cast(count), reinterpret_cast(ids)); } -[[nodiscard]] GLenum draw_framebuffer_binding_query() noexcept +void delete_opengl_textures(std::uint32_t count, const std::uint32_t* ids) noexcept { - return static_cast(pp::renderer::gl::draw_framebuffer_binding_query()); + glDeleteTextures(static_cast(count), reinterpret_cast(ids)); +} + +void bind_opengl_texture(std::uint32_t target, std::uint32_t texture) noexcept +{ + glBindTexture(static_cast(target), static_cast(texture)); +} + +void upload_opengl_texture_2d( + std::uint32_t target, + std::int32_t level, + std::int32_t internal_format, + std::int32_t width, + std::int32_t height, + std::int32_t border, + std::uint32_t pixel_format, + std::uint32_t component_type, + const void* data) noexcept +{ + glTexImage2D( + static_cast(target), + static_cast(level), + static_cast(internal_format), + static_cast(width), + static_cast(height), + static_cast(border), + static_cast(pixel_format), + static_cast(component_type), + data); +} + +void update_opengl_texture_2d_region( + std::uint32_t target, + std::int32_t level, + std::int32_t x, + std::int32_t y, + std::int32_t width, + std::int32_t height, + std::uint32_t pixel_format, + std::uint32_t component_type, + const void* data) noexcept +{ + glTexSubImage2D( + static_cast(target), + static_cast(level), + static_cast(x), + static_cast(y), + static_cast(width), + static_cast(height), + static_cast(pixel_format), + static_cast(component_type), + data); +} + +void generate_opengl_mipmap(std::uint32_t target) noexcept +{ + glGenerateMipmap(static_cast(target)); +} + +void gen_opengl_framebuffers(std::uint32_t count, std::uint32_t* ids) noexcept +{ + glGenFramebuffers(static_cast(count), reinterpret_cast(ids)); +} + +void delete_opengl_framebuffers(std::uint32_t count, const std::uint32_t* ids) noexcept +{ + glDeleteFramebuffers(static_cast(count), reinterpret_cast(ids)); +} + +void query_opengl_integer(std::uint32_t name, std::int32_t* value) noexcept +{ + glGetIntegerv(static_cast(name), reinterpret_cast(value)); +} + +void bind_opengl_framebuffer(std::uint32_t target, std::uint32_t framebuffer) noexcept +{ + glBindFramebuffer(static_cast(target), static_cast(framebuffer)); +} + +void attach_opengl_framebuffer_texture_2d( + std::uint32_t target, + std::uint32_t attachment, + std::uint32_t texture_target, + std::uint32_t texture, + std::int32_t level) noexcept +{ + glFramebufferTexture2D( + static_cast(target), + static_cast(attachment), + static_cast(texture_target), + static_cast(texture), + static_cast(level)); +} + +std::uint32_t check_opengl_framebuffer_status(std::uint32_t target) noexcept +{ + return static_cast(glCheckFramebufferStatus(static_cast(target))); +} + +void read_opengl_pixels( + std::int32_t x, + std::int32_t y, + std::int32_t width, + std::int32_t height, + std::uint32_t pixel_format, + std::uint32_t component_type, + void* pixels) noexcept +{ + glReadPixels( + static_cast(x), + static_cast(y), + static_cast(width), + static_cast(height), + static_cast(pixel_format), + static_cast(component_type), + pixels); } } @@ -162,44 +272,33 @@ Image Texture2D::get_image() const noexcept ret.create(m_width, m_height); App::I->render_task([&] { - bind(); - - GLuint fboID; - glGenFramebuffers(1, &fboID); - if (fboID == 0) + const auto readback = pp::renderer::gl::readback_opengl_texture_2d( + pp::renderer::gl::OpenGlTexture2DReadback { + .texture_id = static_cast(m_tex), + .width = m_width, + .height = m_height, + .format = pp::renderer::gl::rgba8_readback_format(), + .pixels = ret.m_data.get(), + }, + pp::renderer::gl::OpenGlTexture2DReadbackDispatch { + .bind_texture = bind_opengl_texture, + .gen_framebuffers = gen_opengl_framebuffers, + .get_integer = query_opengl_integer, + .bind_framebuffer = bind_opengl_framebuffer, + .framebuffer_texture_2d = attach_opengl_framebuffer_texture_2d, + .check_framebuffer_status = check_opengl_framebuffer_status, + .read_pixels = read_opengl_pixels, + .delete_framebuffers = delete_opengl_framebuffers, + }); + if (!readback.ok()) { + LOG("Texture2D::get_image() failed because: %s", readback.status().message); return; - - GLint oldFboID; - glGetIntegerv(draw_framebuffer_binding_query(), &oldFboID); - - glBindFramebuffer(framebuffer_target(), fboID); - glFramebufferTexture2D( - framebuffer_target(), - static_cast(pp::renderer::gl::framebuffer_color_attachment()), - texture_2d_target(), - m_tex, - 0); - GLenum status = glCheckFramebufferStatus(framebuffer_target()); - if (status == static_cast(pp::renderer::gl::framebuffer_complete_status())) - { - const auto readback = pp::renderer::gl::rgba8_readback_format(); - glReadPixels( - 0, - 0, - m_width, - m_height, - static_cast(readback.pixel_format), - static_cast(readback.component_type), - ret.m_data.get()); } - else - { + + if (!readback.value().pixels_read) { LOG("Texture2D::get_image() failed because: %s", - pp::renderer::gl::framebuffer_status_name(static_cast(status))); + pp::renderer::gl::framebuffer_status_name(readback.value().framebuffer_status)); } - - glBindFramebuffer(framebuffer_target(), oldFboID); - glDeleteFramebuffers(1, &fboID); }); return ret; } @@ -249,17 +348,32 @@ bool Texture2D::create(int width, int height, GLint internal_format, GLint forma App::I->render_task([=] { destroy(); + const auto component_type = pp::renderer::gl::texture_upload_type_for_internal_format( + static_cast(internal_format)); + const auto texture = pp::renderer::gl::allocate_opengl_texture_2d( + pp::renderer::gl::OpenGlTexture2DAllocation { + .width = width, + .height = height, + .internal_format = internal_format, + .pixel_format = static_cast(format), + .component_type = component_type, + .data = data, + }, + pp::renderer::gl::OpenGlTexture2DAllocationDispatch { + .gen_textures = gen_opengl_textures, + .bind_texture = bind_opengl_texture, + .tex_image_2d = upload_opengl_texture_2d, + }); + if (!texture.ok()) { + LOG("Texture2D::create() failed because: %s", texture.status().message); + return; + } + m_width = width; m_height = height; m_format = format; m_iformat = internal_format; - glGenTextures(1, &m_tex); - //LOG("TEX create %d", m_tex); - bind(); - const auto ifmt = static_cast( - pp::renderer::gl::texture_upload_type_for_internal_format(static_cast(internal_format))); - glTexImage2D(texture_2d_target(), 0, internal_format, width, height, 0, format, ifmt, data); - unbind(); + m_tex = static_cast(texture.value()); }); return true; } @@ -281,9 +395,16 @@ void Texture2D::create_mipmaps() { App::I->render_task([this] { - bind(); - glGenerateMipmap(texture_2d_target()); - unbind(); + const auto status = pp::renderer::gl::generate_opengl_texture_2d_mipmaps( + static_cast(m_tex), + pp::renderer::gl::OpenGlTexture2DMipmapDispatch { + .bind_texture = bind_opengl_texture, + .generate_mipmap = generate_opengl_mipmap, + }); + if (!status.ok()) { + LOG("Texture2D::create_mipmaps() failed because: %s", status.message); + return; + } has_mips = true; }); } @@ -331,7 +452,13 @@ void Texture2D::destroy() { App::I->render_task_async([id = m_tex] { - glDeleteTextures(1, &id); + const auto status = pp::renderer::gl::delete_opengl_texture_2d( + static_cast(id), + pp::renderer::gl::OpenGlTexture2DDeleteDispatch { + .delete_textures = delete_opengl_textures, + }); + if (!status.ok()) + LOG("Texture2D::destroy() failed because: %s", status.message); }); m_tex = 0; } @@ -340,30 +467,46 @@ void Texture2D::destroy() void Texture2D::bind() const { assert(App::I->is_render_thread()); - glBindTexture(texture_2d_target(), m_tex); + const auto status = pp::renderer::gl::bind_opengl_texture_2d( + static_cast(m_tex), + pp::renderer::gl::OpenGlTexture2DBindDispatch { + .bind_texture = bind_opengl_texture, + }); + if (!status.ok()) + LOG("Texture2D::bind() failed because: %s", status.message); } void Texture2D::unbind() const { assert(App::I->is_render_thread()); - glBindTexture(texture_2d_target(), 0); + const auto status = pp::renderer::gl::bind_opengl_texture_2d( + 0U, + pp::renderer::gl::OpenGlTexture2DBindDispatch { + .bind_texture = bind_opengl_texture, + }); + if (!status.ok()) + LOG("Texture2D::unbind() failed because: %s", status.message); } void Texture2D::update(const uint8_t* data) { App::I->render_task([this, data] { - bind(); - glTexSubImage2D( - texture_2d_target(), - 0, - 0, - 0, - m_width, - m_height, - m_format, - static_cast(pp::renderer::gl::unsigned_byte_component_type()), - data); + const auto status = pp::renderer::gl::update_opengl_texture_2d( + pp::renderer::gl::OpenGlTexture2DUpdate { + .texture_id = static_cast(m_tex), + .width = m_width, + .height = m_height, + .pixel_format = static_cast(m_format), + .component_type = pp::renderer::gl::unsigned_byte_component_type(), + .data = data, + }, + pp::renderer::gl::OpenGlTexture2DUpdateDispatch { + .bind_texture = bind_opengl_texture, + .tex_sub_image_2d = update_opengl_texture_2d_region, + }); + if (!status.ok()) + LOG("Texture2D::update() failed because: %s", status.message); }); } diff --git a/tests/renderer_gl/capabilities_tests.cpp b/tests/renderer_gl/capabilities_tests.cpp index f97f4da..a96b911 100644 --- a/tests/renderer_gl/capabilities_tests.cpp +++ b/tests/renderer_gl/capabilities_tests.cpp @@ -38,6 +38,39 @@ struct RecordedOpenGlBindingCall { std::uint32_t second = 0; }; +struct RecordedOpenGlTextureImageCall { + std::uint32_t target = 0; + std::int32_t level = 0; + std::int32_t internal_format = 0; + std::int32_t x = 0; + std::int32_t y = 0; + std::int32_t width = 0; + std::int32_t height = 0; + std::int32_t border = 0; + std::uint32_t pixel_format = 0; + std::uint32_t component_type = 0; + const void* data = nullptr; + bool sub_image = false; +}; + +struct RecordedOpenGlFramebufferAttachmentCall { + std::uint32_t target = 0; + std::uint32_t attachment = 0; + std::uint32_t texture_target = 0; + std::uint32_t texture = 0; + std::int32_t level = 0; +}; + +struct RecordedOpenGlReadPixelsCall { + std::int32_t x = 0; + std::int32_t y = 0; + std::int32_t width = 0; + std::int32_t height = 0; + std::uint32_t pixel_format = 0; + std::uint32_t component_type = 0; + void* pixels = nullptr; +}; + std::vector recorded_state_calls; std::vector recorded_string_queries; std::vector recorded_clear_calls; @@ -47,6 +80,18 @@ std::vector recorded_integer_queries; std::vector recorded_float_queries; std::vector recorded_active_texture_calls; std::vector recorded_binding_calls; +std::vector recorded_generated_texture_counts; +std::vector recorded_deleted_textures; +std::vector recorded_texture_image_calls; +std::vector recorded_mipmap_targets; +std::vector recorded_generated_framebuffer_counts; +std::vector recorded_deleted_framebuffers; +std::vector recorded_framebuffer_attachment_calls; +std::vector recorded_framebuffer_status_queries; +std::vector recorded_read_pixels_calls; +std::uint32_t next_texture_id = 91U; +std::uint32_t next_framebuffer_id = 44U; +std::uint32_t configured_framebuffer_status = 0x8CD5U; void record_enable(std::uint32_t state) noexcept { @@ -236,6 +281,132 @@ void record_bind_sampler(std::uint32_t unit, std::uint32_t sampler) noexcept }); } +void record_gen_textures(std::uint32_t count, std::uint32_t* ids) noexcept +{ + recorded_generated_texture_counts.push_back(count); + for (std::uint32_t i = 0U; i < count; ++i) { + ids[i] = next_texture_id + i; + } +} + +void record_delete_textures(std::uint32_t count, const std::uint32_t* ids) noexcept +{ + for (std::uint32_t i = 0U; i < count; ++i) { + recorded_deleted_textures.push_back(ids[i]); + } +} + +void record_tex_image_2d( + std::uint32_t target, + std::int32_t level, + std::int32_t internal_format, + std::int32_t width, + std::int32_t height, + std::int32_t border, + std::uint32_t pixel_format, + std::uint32_t component_type, + const void* data) noexcept +{ + recorded_texture_image_calls.push_back(RecordedOpenGlTextureImageCall { + .target = target, + .level = level, + .internal_format = internal_format, + .width = width, + .height = height, + .border = border, + .pixel_format = pixel_format, + .component_type = component_type, + .data = data, + }); +} + +void record_tex_sub_image_2d( + std::uint32_t target, + std::int32_t level, + std::int32_t x, + std::int32_t y, + std::int32_t width, + std::int32_t height, + std::uint32_t pixel_format, + std::uint32_t component_type, + const void* data) noexcept +{ + recorded_texture_image_calls.push_back(RecordedOpenGlTextureImageCall { + .target = target, + .level = level, + .x = x, + .y = y, + .width = width, + .height = height, + .pixel_format = pixel_format, + .component_type = component_type, + .data = data, + .sub_image = true, + }); +} + +void record_generate_mipmap(std::uint32_t target) noexcept +{ + recorded_mipmap_targets.push_back(target); +} + +void record_gen_framebuffers(std::uint32_t count, std::uint32_t* ids) noexcept +{ + recorded_generated_framebuffer_counts.push_back(count); + for (std::uint32_t i = 0U; i < count; ++i) { + ids[i] = next_framebuffer_id + i; + } +} + +void record_delete_framebuffers(std::uint32_t count, const std::uint32_t* ids) noexcept +{ + for (std::uint32_t i = 0U; i < count; ++i) { + recorded_deleted_framebuffers.push_back(ids[i]); + } +} + +void record_framebuffer_texture_2d( + std::uint32_t target, + std::uint32_t attachment, + std::uint32_t texture_target, + std::uint32_t texture, + std::int32_t level) noexcept +{ + recorded_framebuffer_attachment_calls.push_back(RecordedOpenGlFramebufferAttachmentCall { + .target = target, + .attachment = attachment, + .texture_target = texture_target, + .texture = texture, + .level = level, + }); +} + +std::uint32_t record_check_framebuffer_status(std::uint32_t target) noexcept +{ + recorded_framebuffer_status_queries.push_back(target); + return configured_framebuffer_status; +} + +void record_read_pixels( + std::int32_t x, + std::int32_t y, + std::int32_t width, + std::int32_t height, + std::uint32_t pixel_format, + std::uint32_t component_type, + void* pixels) noexcept +{ + recorded_read_pixels_calls.push_back(RecordedOpenGlReadPixelsCall { + .x = x, + .y = y, + .width = width, + .height = height, + .pixel_format = pixel_format, + .component_type = component_type, + .pixels = pixels, + }); +} + void detects_common_extension_capabilities(pp::tests::Harness& h) { constexpr std::array extensions { @@ -1317,6 +1488,261 @@ void rejects_incomplete_buffer_clear_dispatch(pp::tests::Harness& h) PP_EXPECT(h, status.code == pp::foundation::StatusCode::invalid_argument); } +void allocates_texture_2d_through_dispatch(pp::tests::Harness& h) +{ + recorded_generated_texture_counts.clear(); + recorded_binding_calls.clear(); + recorded_texture_image_calls.clear(); + next_texture_id = 91U; + const std::array pixels { 1U, 2U, 3U, 4U }; + + const auto texture = pp::renderer::gl::allocate_opengl_texture_2d( + pp::renderer::gl::OpenGlTexture2DAllocation { + .width = 8, + .height = 4, + .internal_format = 0x8058, + .pixel_format = 0x1908U, + .component_type = 0x1401U, + .data = pixels.data(), + }, + pp::renderer::gl::OpenGlTexture2DAllocationDispatch { + .gen_textures = record_gen_textures, + .bind_texture = record_bind_texture, + .tex_image_2d = record_tex_image_2d, + }); + + PP_EXPECT(h, texture.ok()); + PP_EXPECT(h, texture.value() == 91U); + PP_EXPECT(h, recorded_generated_texture_counts.size() == 1U); + PP_EXPECT(h, recorded_generated_texture_counts[0] == 1U); + PP_EXPECT(h, recorded_binding_calls.size() == 2U); + PP_EXPECT(h, recorded_binding_calls[0].kind == RecordedOpenGlBindingCall::Kind::bind_texture); + PP_EXPECT(h, recorded_binding_calls[0].first == 0x0DE1U); + PP_EXPECT(h, recorded_binding_calls[0].second == 91U); + PP_EXPECT(h, recorded_binding_calls[1].second == 0U); + PP_EXPECT(h, recorded_texture_image_calls.size() == 1U); + PP_EXPECT(h, recorded_texture_image_calls[0].target == 0x0DE1U); + PP_EXPECT(h, recorded_texture_image_calls[0].width == 8); + PP_EXPECT(h, recorded_texture_image_calls[0].height == 4); + PP_EXPECT(h, recorded_texture_image_calls[0].data == pixels.data()); +} + +void rejects_invalid_texture_2d_allocation(pp::tests::Harness& h) +{ + const auto missing_dispatch = pp::renderer::gl::allocate_opengl_texture_2d( + pp::renderer::gl::OpenGlTexture2DAllocation { + .width = 1, + .height = 1, + .internal_format = 0x8058, + .pixel_format = 0x1908U, + .component_type = 0x1401U, + }, + pp::renderer::gl::OpenGlTexture2DAllocationDispatch { + .gen_textures = record_gen_textures, + .bind_texture = record_bind_texture, + }); + const auto invalid_dimensions = pp::renderer::gl::allocate_opengl_texture_2d( + pp::renderer::gl::OpenGlTexture2DAllocation { + .width = 0, + .height = 1, + .internal_format = 0x8058, + .pixel_format = 0x1908U, + .component_type = 0x1401U, + }, + pp::renderer::gl::OpenGlTexture2DAllocationDispatch { + .gen_textures = record_gen_textures, + .bind_texture = record_bind_texture, + .tex_image_2d = record_tex_image_2d, + }); + + PP_EXPECT(h, !missing_dispatch.ok()); + PP_EXPECT(h, missing_dispatch.status().code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(h, !invalid_dimensions.ok()); + PP_EXPECT(h, invalid_dimensions.status().code == pp::foundation::StatusCode::invalid_argument); +} + +void deletes_and_binds_texture_2d_through_dispatch(pp::tests::Harness& h) +{ + recorded_deleted_textures.clear(); + recorded_binding_calls.clear(); + + const auto delete_status = pp::renderer::gl::delete_opengl_texture_2d( + 27U, + pp::renderer::gl::OpenGlTexture2DDeleteDispatch { + .delete_textures = record_delete_textures, + }); + const auto bind_status = pp::renderer::gl::bind_opengl_texture_2d( + 27U, + pp::renderer::gl::OpenGlTexture2DBindDispatch { + .bind_texture = record_bind_texture, + }); + + PP_EXPECT(h, delete_status.ok()); + PP_EXPECT(h, recorded_deleted_textures.size() == 1U); + PP_EXPECT(h, recorded_deleted_textures[0] == 27U); + PP_EXPECT(h, bind_status.ok()); + PP_EXPECT(h, recorded_binding_calls.size() == 1U); + PP_EXPECT(h, recorded_binding_calls[0].first == 0x0DE1U); + PP_EXPECT(h, recorded_binding_calls[0].second == 27U); +} + +void updates_texture_2d_through_dispatch(pp::tests::Harness& h) +{ + recorded_binding_calls.clear(); + recorded_texture_image_calls.clear(); + const std::array pixels { 9U, 8U, 7U, 6U }; + + const auto status = pp::renderer::gl::update_opengl_texture_2d( + pp::renderer::gl::OpenGlTexture2DUpdate { + .texture_id = 31U, + .width = 2, + .height = 2, + .pixel_format = 0x1908U, + .component_type = 0x1401U, + .data = pixels.data(), + }, + pp::renderer::gl::OpenGlTexture2DUpdateDispatch { + .bind_texture = record_bind_texture, + .tex_sub_image_2d = record_tex_sub_image_2d, + }); + + PP_EXPECT(h, status.ok()); + PP_EXPECT(h, recorded_binding_calls.size() == 1U); + PP_EXPECT(h, recorded_binding_calls[0].second == 31U); + PP_EXPECT(h, recorded_texture_image_calls.size() == 1U); + PP_EXPECT(h, recorded_texture_image_calls[0].sub_image); + PP_EXPECT(h, recorded_texture_image_calls[0].width == 2); + PP_EXPECT(h, recorded_texture_image_calls[0].height == 2); + PP_EXPECT(h, recorded_texture_image_calls[0].data == pixels.data()); +} + +void generates_texture_2d_mipmaps_through_dispatch(pp::tests::Harness& h) +{ + recorded_binding_calls.clear(); + recorded_mipmap_targets.clear(); + + const auto status = pp::renderer::gl::generate_opengl_texture_2d_mipmaps( + 41U, + pp::renderer::gl::OpenGlTexture2DMipmapDispatch { + .bind_texture = record_bind_texture, + .generate_mipmap = record_generate_mipmap, + }); + + PP_EXPECT(h, status.ok()); + PP_EXPECT(h, recorded_binding_calls.size() == 2U); + PP_EXPECT(h, recorded_binding_calls[0].second == 41U); + PP_EXPECT(h, recorded_binding_calls[1].second == 0U); + PP_EXPECT(h, recorded_mipmap_targets.size() == 1U); + PP_EXPECT(h, recorded_mipmap_targets[0] == 0x0DE1U); +} + +void reads_back_texture_2d_through_framebuffer_dispatch(pp::tests::Harness& h) +{ + recorded_generated_framebuffer_counts.clear(); + recorded_deleted_framebuffers.clear(); + recorded_binding_calls.clear(); + recorded_framebuffer_attachment_calls.clear(); + recorded_framebuffer_status_queries.clear(); + recorded_read_pixels_calls.clear(); + next_framebuffer_id = 44U; + configured_framebuffer_status = 0x8CD5U; + std::array pixels {}; + + const auto result = pp::renderer::gl::readback_opengl_texture_2d( + pp::renderer::gl::OpenGlTexture2DReadback { + .texture_id = 56U, + .width = 2, + .height = 2, + .format = pp::renderer::gl::rgba8_readback_format(), + .pixels = pixels.data(), + }, + pp::renderer::gl::OpenGlTexture2DReadbackDispatch { + .bind_texture = record_bind_texture, + .gen_framebuffers = record_gen_framebuffers, + .get_integer = record_get_integer, + .bind_framebuffer = record_bind_framebuffer, + .framebuffer_texture_2d = record_framebuffer_texture_2d, + .check_framebuffer_status = record_check_framebuffer_status, + .read_pixels = record_read_pixels, + .delete_framebuffers = record_delete_framebuffers, + }); + + PP_EXPECT(h, result.ok()); + PP_EXPECT(h, result.value().pixels_read); + PP_EXPECT(h, result.value().framebuffer_status == 0x8CD5U); + PP_EXPECT(h, recorded_generated_framebuffer_counts.size() == 1U); + PP_EXPECT(h, recorded_generated_framebuffer_counts[0] == 1U); + PP_EXPECT(h, recorded_binding_calls.size() == 3U); + PP_EXPECT(h, recorded_binding_calls[0].kind == RecordedOpenGlBindingCall::Kind::bind_texture); + PP_EXPECT(h, recorded_binding_calls[0].second == 56U); + PP_EXPECT(h, recorded_binding_calls[1].kind == RecordedOpenGlBindingCall::Kind::bind_framebuffer); + PP_EXPECT(h, recorded_binding_calls[1].first == 0x8D40U); + PP_EXPECT(h, recorded_binding_calls[1].second == 44U); + PP_EXPECT(h, recorded_binding_calls[2].kind == RecordedOpenGlBindingCall::Kind::bind_framebuffer); + PP_EXPECT(h, recorded_binding_calls[2].second == 7U); + PP_EXPECT(h, recorded_framebuffer_attachment_calls.size() == 1U); + PP_EXPECT(h, recorded_framebuffer_attachment_calls[0].attachment == 0x8CE0U); + PP_EXPECT(h, recorded_framebuffer_attachment_calls[0].texture == 56U); + PP_EXPECT(h, recorded_read_pixels_calls.size() == 1U); + PP_EXPECT(h, recorded_read_pixels_calls[0].pixels == pixels.data()); + PP_EXPECT(h, recorded_deleted_framebuffers.size() == 1U); + PP_EXPECT(h, recorded_deleted_framebuffers[0] == 44U); +} + +void reports_incomplete_texture_2d_readback_framebuffer(pp::tests::Harness& h) +{ + recorded_deleted_framebuffers.clear(); + recorded_read_pixels_calls.clear(); + next_framebuffer_id = 45U; + configured_framebuffer_status = 0x8CD6U; + std::array pixels {}; + + const auto result = pp::renderer::gl::readback_opengl_texture_2d( + pp::renderer::gl::OpenGlTexture2DReadback { + .texture_id = 57U, + .width = 2, + .height = 2, + .format = pp::renderer::gl::rgba8_readback_format(), + .pixels = pixels.data(), + }, + pp::renderer::gl::OpenGlTexture2DReadbackDispatch { + .bind_texture = record_bind_texture, + .gen_framebuffers = record_gen_framebuffers, + .get_integer = record_get_integer, + .bind_framebuffer = record_bind_framebuffer, + .framebuffer_texture_2d = record_framebuffer_texture_2d, + .check_framebuffer_status = record_check_framebuffer_status, + .read_pixels = record_read_pixels, + .delete_framebuffers = record_delete_framebuffers, + }); + + PP_EXPECT(h, result.ok()); + PP_EXPECT(h, !result.value().pixels_read); + PP_EXPECT(h, result.value().framebuffer_status == 0x8CD6U); + PP_EXPECT(h, recorded_read_pixels_calls.empty()); + PP_EXPECT(h, recorded_deleted_framebuffers.back() == 45U); +} + +void rejects_incomplete_texture_2d_readback_dispatch(pp::tests::Harness& h) +{ + std::array pixels {}; + const auto result = pp::renderer::gl::readback_opengl_texture_2d( + pp::renderer::gl::OpenGlTexture2DReadback { + .texture_id = 1U, + .width = 1, + .height = 1, + .format = pp::renderer::gl::rgba8_readback_format(), + .pixels = pixels.data(), + }, + pp::renderer::gl::OpenGlTexture2DReadbackDispatch { + .bind_texture = record_bind_texture, + .gen_framebuffers = record_gen_framebuffers, + }); + + PP_EXPECT(h, !result.ok()); + PP_EXPECT(h, result.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( @@ -1711,6 +2137,14 @@ int main() harness.run("rejects_incomplete_generic_capability_dispatch", rejects_incomplete_generic_capability_dispatch); harness.run("applies_buffer_clear_dispatch", applies_buffer_clear_dispatch); harness.run("rejects_incomplete_buffer_clear_dispatch", rejects_incomplete_buffer_clear_dispatch); + harness.run("allocates_texture_2d_through_dispatch", allocates_texture_2d_through_dispatch); + harness.run("rejects_invalid_texture_2d_allocation", rejects_invalid_texture_2d_allocation); + harness.run("deletes_and_binds_texture_2d_through_dispatch", deletes_and_binds_texture_2d_through_dispatch); + harness.run("updates_texture_2d_through_dispatch", updates_texture_2d_through_dispatch); + harness.run("generates_texture_2d_mipmaps_through_dispatch", generates_texture_2d_mipmaps_through_dispatch); + harness.run("reads_back_texture_2d_through_framebuffer_dispatch", reads_back_texture_2d_through_framebuffer_dispatch); + harness.run("reports_incomplete_texture_2d_readback_framebuffer", reports_incomplete_texture_2d_readback_framebuffer); + harness.run("rejects_incomplete_texture_2d_readback_dispatch", rejects_incomplete_texture_2d_readback_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);