From 52afca38aab1411cc0973565fda026e74c64d1af Mon Sep 17 00:00:00 2001 From: Michael Fabian 'Xaymar' Dirks Date: Fri, 2 Dec 2022 03:59:25 +0100 Subject: [PATCH] gfx/mipmapper: Move gs-mipmapper into proper directory This is not an obs feature, so it shouldn't be in the obs directory. --- CMakeLists.txt | 4 +- source/filters/filter-color-grade.hpp | 2 +- source/filters/filter-transform.hpp | 4 +- source/gfx/gfx-mipmapper.cpp | 351 ++++++++++++++++++++++++++ source/gfx/gfx-mipmapper.hpp | 55 ++++ 5 files changed, 411 insertions(+), 5 deletions(-) create mode 100644 source/gfx/gfx-mipmapper.cpp create mode 100644 source/gfx/gfx-mipmapper.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2db60dd..609b1d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1048,6 +1048,8 @@ list(APPEND PROJECT_PRIVATE_SOURCE "source/util/util-threadpool.hpp" "source/gfx/gfx-util.hpp" "source/gfx/gfx-util.cpp" + "source/gfx/gfx-mipmapper.hpp" + "source/gfx/gfx-mipmapper.cpp" "source/gfx/gfx-opengl.hpp" "source/gfx/gfx-opengl.cpp" "source/gfx/gfx-source-texture.hpp" @@ -1065,8 +1067,6 @@ list(APPEND PROJECT_PRIVATE_SOURCE "source/obs/gs/gs-indexbuffer.hpp" "source/obs/gs/gs-indexbuffer.cpp" "source/obs/gs/gs-limits.hpp" - "source/obs/gs/gs-mipmapper.hpp" - "source/obs/gs/gs-mipmapper.cpp" "source/obs/gs/gs-rendertarget.hpp" "source/obs/gs/gs-rendertarget.cpp" "source/obs/gs/gs-sampler.hpp" diff --git a/source/filters/filter-color-grade.hpp b/source/filters/filter-color-grade.hpp index 8a6f401..7c6e2a0 100644 --- a/source/filters/filter-color-grade.hpp +++ b/source/filters/filter-color-grade.hpp @@ -18,10 +18,10 @@ */ #pragma once +#include "gfx/gfx-mipmapper.hpp" #include "gfx/lut/gfx-lut-consumer.hpp" #include "gfx/lut/gfx-lut-producer.hpp" #include "gfx/lut/gfx-lut.hpp" -#include "obs/gs/gs-mipmapper.hpp" #include "obs/gs/gs-rendertarget.hpp" #include "obs/gs/gs-texture.hpp" #include "obs/gs/gs-vertexbuffer.hpp" diff --git a/source/filters/filter-transform.hpp b/source/filters/filter-transform.hpp index ccac8d5..080e9ff 100644 --- a/source/filters/filter-transform.hpp +++ b/source/filters/filter-transform.hpp @@ -19,8 +19,8 @@ #pragma once #include "common.hpp" +#include "gfx/gfx-mipmapper.hpp" #include "gfx/gfx-util.hpp" -#include "obs/gs/gs-mipmapper.hpp" #include "obs/gs/gs-rendertarget.hpp" #include "obs/gs/gs-texture.hpp" #include "obs/gs/gs-vertexbuffer.hpp" @@ -70,7 +70,7 @@ namespace streamfx::filter::transform { // Mip-mapping bool _mipmap_enabled; bool _mipmap_rendered; - streamfx::obs::gs::mipmapper _mipmapper; + streamfx::gfx::mipmapper _mipmapper; std::shared_ptr _mipmap_texture; // Input diff --git a/source/gfx/gfx-mipmapper.cpp b/source/gfx/gfx-mipmapper.cpp new file mode 100644 index 0000000..75bfcd6 --- /dev/null +++ b/source/gfx/gfx-mipmapper.cpp @@ -0,0 +1,351 @@ +/* + * Modern effects for a modern Streamer + * Copyright (C) 2017 Michael Fabian Dirks + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "gfx-mipmapper.hpp" +#include "obs/gs/gs-helper.hpp" +#include "plugin.hpp" + +#include "warning-disable.hpp" +#include +#include +// Direct3D 11 +#ifdef _WIN32 +#include +#include +#include +#include +#endif +// OpenGL +#include "glad/gl.h" +#include "warning-enable.hpp" + +#ifdef _WIN32 +struct d3d_info { + ID3D11Device* device = nullptr; + ID3D11DeviceContext* context = nullptr; + ID3D11Resource* target = nullptr; +}; + +void d3d_initialize(d3d_info& info, std::shared_ptr source, + std::shared_ptr target) +{ + info.target = reinterpret_cast(gs_texture_get_obj(target->get_object())); + info.device = reinterpret_cast(gs_get_device_obj()); + info.device->GetImmediateContext(&info.context); +} + +void d3d_copy_subregion(d3d_info& info, std::shared_ptr source, uint32_t mip_level, + uint32_t width, uint32_t height) +{ + D3D11_BOX box = {0, 0, 0, width, height, 1}; + auto source_ref = reinterpret_cast(gs_texture_get_obj(source->get_object())); + info.context->CopySubresourceRegion(info.target, mip_level, 0, 0, 0, source_ref, 0, &box); +} + +#endif + +struct opengl_info { + GLuint target = 0; + GLuint fbo = 0; +}; + +std::string opengl_translate_error(GLenum error) +{ +#define TRANSLATE_CASE(X) \ + case X: \ + return #X; + + switch (error) { + TRANSLATE_CASE(GL_NO_ERROR); + TRANSLATE_CASE(GL_INVALID_ENUM); + TRANSLATE_CASE(GL_INVALID_VALUE); + TRANSLATE_CASE(GL_INVALID_OPERATION); + TRANSLATE_CASE(GL_STACK_OVERFLOW); + TRANSLATE_CASE(GL_STACK_UNDERFLOW); + TRANSLATE_CASE(GL_OUT_OF_MEMORY); + TRANSLATE_CASE(GL_INVALID_FRAMEBUFFER_OPERATION); + } + + return std::to_string(error); +#undef TRANSLATE_CASE +} + +std::string opengl_translate_framebuffer_status(GLenum error) +{ +#define TRANSLATE_CASE(X) \ + case X: \ + return #X; + + switch (error) { + TRANSLATE_CASE(GL_FRAMEBUFFER_COMPLETE); + TRANSLATE_CASE(GL_FRAMEBUFFER_UNDEFINED); + TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT); + TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT); + TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER); + TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER); + TRANSLATE_CASE(GL_FRAMEBUFFER_UNSUPPORTED); + TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE); + TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS); + } + + return std::to_string(error); +#undef TRANSLATE_CASE +} + +#define D_OPENGL_CHECK_ERROR(FUNCTION) \ + if (auto err = glGetError(); err != GL_NO_ERROR) { \ + std::stringstream sstr; \ + sstr << opengl_translate_error(err) << " = " << FUNCTION; \ + throw std::runtime_error(sstr.str()); \ + } + +#define D_OPENGL_CHECK_FRAMEBUFFERSTATUS(BUFFER, FUNCTION) \ + if (auto err = glCheckFramebufferStatus(BUFFER); err != GL_FRAMEBUFFER_COMPLETE) { \ + std::stringstream sstr; \ + sstr << opengl_translate_framebuffer_status(err) << " = " << FUNCTION; \ + throw std::runtime_error(sstr.str()); \ + } + +void opengl_initialize(opengl_info& info, std::shared_ptr source, + std::shared_ptr target) +{ + info.target = *reinterpret_cast(gs_texture_get_obj(target->get_object())); + + glGenFramebuffers(1, &info.fbo); +} + +void opengl_finalize(opengl_info& info) +{ + glDeleteFramebuffers(1, &info.fbo); +} + +void opengl_copy_subregion(opengl_info& info, std::shared_ptr source, uint32_t mip_level, + uint32_t width, uint32_t height) +{ + GLuint source_ref = *reinterpret_cast(gs_texture_get_obj(source->get_object())); + + // Source -> Texture Unit 0, Read Color Framebuffer + glActiveTexture(GL_TEXTURE0); + D_OPENGL_CHECK_ERROR("glActiveTexture(GL_TEXTURE0);"); + glBindTexture(GL_TEXTURE_2D, source_ref); + D_OPENGL_CHECK_ERROR("glBindTexture(GL_TEXTURE_2D, origin);"); + glBindFramebuffer(GL_READ_FRAMEBUFFER, info.fbo); + D_OPENGL_CHECK_ERROR("glBindFramebuffer(GL_READ_FRAMEBUFFER, info.fbo);"); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, source_ref, + 0); // Origin is a render target, not a texture + D_OPENGL_CHECK_ERROR( + "glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, origin, mip_level);"); + D_OPENGL_CHECK_FRAMEBUFFERSTATUS( + GL_READ_FRAMEBUFFER, + "glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, origin, mip_level);"); + + // Target -> Texture Unit 1 + glActiveTexture(GL_TEXTURE1); + D_OPENGL_CHECK_ERROR("glActiveTexture(GL_TEXTURE1);"); + glBindTexture(GL_TEXTURE_2D, info.target); + D_OPENGL_CHECK_ERROR("glBindTexture(GL_TEXTURE_2D, info.target);"); + + // Copy Data + glCopyTexSubImage2D(GL_TEXTURE_2D, static_cast(mip_level), 0, 0, 0, 0, static_cast(width), + static_cast(height)); + D_OPENGL_CHECK_ERROR("glCopyTexSubImage2D(GL_TEXTURE_2D, mip_level, 0, 0, 0, 0, width, height);"); + + // Target -/-> Texture Unit 1 + glActiveTexture(GL_TEXTURE1); + D_OPENGL_CHECK_ERROR("glActiveTexture(GL_TEXTURE1);"); + glBindTexture(GL_TEXTURE_2D, 0); + D_OPENGL_CHECK_ERROR("glBindTexture(GL_TEXTURE_2D, 0);"); + + // Source -/-> Texture Unit 0, Read Color Framebuffer + glActiveTexture(GL_TEXTURE0); + D_OPENGL_CHECK_ERROR("glActiveTexture(GL_TEXTURE0);"); + glBindTexture(GL_TEXTURE_2D, 0); + D_OPENGL_CHECK_ERROR("glBindTexture(GL_TEXTURE_2D, 0);"); + glBindFramebuffer(GL_READ_FRAMEBUFFER, info.fbo); + D_OPENGL_CHECK_ERROR("glBindFramebuffer(GL_READ_FRAMEBUFFER, info.fbo);"); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + D_OPENGL_CHECK_ERROR("glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);"); + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + D_OPENGL_CHECK_ERROR("glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);"); +} + +streamfx::gfx::mipmapper::~mipmapper() +{ + _rt.reset(); + _effect.reset(); +} + +streamfx::gfx::mipmapper::mipmapper() : _gfx_util(::streamfx::gfx::util::get()) +{ + auto gctx = streamfx::obs::gs::context(); + + { + auto file = streamfx::data_file_path("effects/mipgen.effect"); + try { + _effect = streamfx::obs::gs::effect::create(file); + } catch (const std::exception& ex) { + DLOG_ERROR("Error loading '%s': %s", file.generic_u8string().c_str(), ex.what()); + } + } +} + +uint32_t streamfx::gfx::mipmapper::calculate_max_mip_level(uint32_t width, uint32_t height) +{ + return static_cast( + 1 + std::lroundl(floor(log2(std::max(static_cast(width), static_cast(height)))))); +} + +void streamfx::gfx::mipmapper::rebuild(std::shared_ptr source, + std::shared_ptr target) +{ + { // Validate arguments and structure. + if (!source || !target) + return; // Do nothing if source or target are missing. + + if (!_effect) + return; // Do nothing if the necessary data failed to load. + + // Ensure texture sizes match + if ((source->get_width() != target->get_width()) || (source->get_height() != target->get_height())) { + throw std::invalid_argument("source and target must have same size"); + } + + // Ensure texture types match + if ((source->get_type() != target->get_type())) { + throw std::invalid_argument("source and target must have same type"); + } + + // Ensure texture formats match + if ((source->get_color_format() != target->get_color_format())) { + throw std::invalid_argument("source and target must have same format"); + } + } + + // Get a unique lock on the graphics context. + auto gctx = streamfx::obs::gs::context(); + + // Do we need to recreate the render target for a different format? + if ((!_rt) || (source->get_color_format() != _rt->get_color_format())) { + _rt = std::make_unique(source->get_color_format(), GS_ZS_NONE); + } + + // Initialize API Handlers. + opengl_info oglinfo; + if (gs_get_device_type() == GS_DEVICE_OPENGL) { + opengl_initialize(oglinfo, source, target); + } +#ifdef _WIN32 + d3d_info d3dinfo; + if (gs_get_device_type() == GS_DEVICE_DIRECT3D_11) { + d3d_initialize(d3dinfo, source, target); + } +#endif + + // Use different methods for different types of textures. + if (source->get_type() == streamfx::obs::gs::texture::type::Normal) { + uint32_t width = source->get_width(); + uint32_t height = source->get_height(); + size_t max_mip_level = calculate_max_mip_level(width, height); + + { +#ifdef ENABLE_PROFILING + auto cctr = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, + "Mip Level %" PRId64 "", 0); +#endif + + // Retrieve maximum mip map level. +#ifdef _WIN32 + if (gs_get_device_type() == GS_DEVICE_DIRECT3D_11) { + d3d_copy_subregion(d3dinfo, source, 0, width, height); + } +#endif + if (gs_get_device_type() == GS_DEVICE_OPENGL) { + opengl_copy_subregion(oglinfo, source, 0, width, height); + } + } + + // Set up rendering state. + gs_blend_state_push(); + gs_reset_blend_state(); + gs_enable_blending(false); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + gs_enable_color(true, true, true, true); + gs_enable_depth_test(false); + gs_enable_stencil_test(false); + gs_enable_stencil_write(false); + gs_set_cull_mode(GS_NEITHER); + + // sRGB support. + bool old_srgb = gs_framebuffer_srgb_enabled(); + gs_enable_framebuffer_srgb(gs_get_linear_srgb()); + + // Render each mip map level. + for (size_t mip = 1; mip < max_mip_level; mip++) { +#ifdef ENABLE_PROFILING + auto cctr = streamfx::obs::gs::debug_marker(streamfx::obs::gs::debug_color_azure_radiance, + "Mip Level %" PRIuMAX, mip); +#endif + + uint32_t cwidth = std::max(width >> mip, 1); + uint32_t cheight = std::max(height >> mip, 1); + float_t iwidth = 1.f / static_cast(cwidth); + float_t iheight = 1.f / static_cast(cheight); + + try { + auto op = _rt->render(cwidth, cheight); + gs_ortho(0, 1, 0, 1, 0, 1); + + _effect.get_parameter("image").set_texture(target, gs_get_linear_srgb()); + _effect.get_parameter("imageTexel").set_float2(iwidth, iheight); + _effect.get_parameter("level").set_int(int32_t(mip - 1)); + while (gs_effect_loop(_effect.get_object(), "Draw")) { + _gfx_util->draw_fullscreen_triangle(); + } + } catch (...) { + } + + // Copy from the render target to the target mip level. +#ifdef _WIN32 + if (gs_get_device_type() == GS_DEVICE_DIRECT3D_11) { + d3d_copy_subregion(d3dinfo, _rt->get_texture(), static_cast(mip), cwidth, cheight); + } +#endif + if (gs_get_device_type() == GS_DEVICE_OPENGL) { + opengl_copy_subregion(oglinfo, _rt->get_texture(), static_cast(mip), cwidth, cheight); + } + } + + // Clean up rendering state. + gs_enable_framebuffer_srgb(old_srgb); + gs_blend_state_pop(); + + } else { + throw std::runtime_error("Only 2D Textures support Mip-mapping."); + } + + // Finalize API handlers. + if (gs_get_device_type() == GS_DEVICE_OPENGL) { + opengl_finalize(oglinfo); + } +#ifdef _WIN32 + if (gs_get_device_type() == GS_DEVICE_DIRECT3D_11) { + //d3d_finalize(d3dinfo); + } +#endif +} diff --git a/source/gfx/gfx-mipmapper.hpp b/source/gfx/gfx-mipmapper.hpp new file mode 100644 index 0000000..4f13745 --- /dev/null +++ b/source/gfx/gfx-mipmapper.hpp @@ -0,0 +1,55 @@ +/* + * Modern effects for a modern Streamer + * Copyright (C) 2017 Michael Fabian Dirks + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#pragma once +#include "common.hpp" +#include "gfx/gfx-util.hpp" +#include "obs/gs/gs-effect.hpp" +#include "obs/gs/gs-rendertarget.hpp" +#include "obs/gs/gs-texture.hpp" +#include "obs/gs/gs-vertexbuffer.hpp" + +/* gs::mipmapper is an attempt at adding dynamic mip-map generation to a software + * which only supports static mip-maps. It is effectively an incredibly bad hack + * instead of a proper solution - can break any time and likely already has. + * + * Needless to say, dynamic mip-map generation costs a lot of GPU time, especially + * when things need to be synchronized. In the ideal case we would just render + * straight to the mip level, but this is not possible in DirectX 11 and OpenGL. + * + * So instead we render to a render target and copy from there to the actual + * resource. Super wasteful, but what else can we actually do? + */ + +namespace streamfx::gfx { + class mipmapper { + std::unique_ptr _rt; + streamfx::obs::gs::effect _effect; + std::shared_ptr _gfx_util; + + public: + ~mipmapper(); + mipmapper(); + + uint32_t calculate_max_mip_level(uint32_t width, uint32_t height); + + void rebuild(std::shared_ptr source, + std::shared_ptr target); + }; +} // namespace streamfx::gfx