// AUTOGENERATED COPYRIGHT HEADER START // Copyright (C) 2017-2023 Michael Fabian 'Xaymar' Dirks // Copyright (C) 2022 lainon // AUTOGENERATED COPYRIGHT HEADER END #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; }; static void d3d_initialize(d3d_info& info, std::shared_ptr source, std::shared_ptr target) { info.target = reinterpret_cast(gs_texture_get_obj(*target)); info.device = reinterpret_cast(gs_get_device_obj()); info.device->GetImmediateContext(&info.context); } static void d3d_copy_subregion(d3d_info& info, gs_texture_t* 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)); info.context->CopySubresourceRegion(info.target, mip_level, 0, 0, 0, source_ref, 0, &box); } #endif struct opengl_info { GLuint target = 0; GLuint fbo = 0; }; static std::string opengl_translate_error(GLenum error) { #define TRANSLATE_CASE(X) \ case X: \ return #X; switch (error) { TRANSLATE_CASE(GL_NO_ERROR) break; TRANSLATE_CASE(GL_INVALID_ENUM) break; TRANSLATE_CASE(GL_INVALID_VALUE) break; TRANSLATE_CASE(GL_INVALID_OPERATION) break; TRANSLATE_CASE(GL_STACK_OVERFLOW) break; TRANSLATE_CASE(GL_STACK_UNDERFLOW) break; TRANSLATE_CASE(GL_OUT_OF_MEMORY) break; TRANSLATE_CASE(GL_INVALID_FRAMEBUFFER_OPERATION) break; } return std::to_string(error); #undef TRANSLATE_CASE } static std::string opengl_translate_framebuffer_status(GLenum error) { #define TRANSLATE_CASE(X) \ case X: \ return #X; switch (error) { TRANSLATE_CASE(GL_FRAMEBUFFER_COMPLETE) break; TRANSLATE_CASE(GL_FRAMEBUFFER_UNDEFINED) break; TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT) break; TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT) break; TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER) break; TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER) break; TRANSLATE_CASE(GL_FRAMEBUFFER_UNSUPPORTED) break; TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE) break; TRANSLATE_CASE(GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS) break; } 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()); \ } static void opengl_initialize(opengl_info& info, std::shared_ptr source, std::shared_ptr target) { info.target = *reinterpret_cast(gs_texture_get_obj(*target)); glGenFramebuffers(1, &info.fbo); } static void opengl_finalize(opengl_info& info) { glDeleteFramebuffers(1, &info.fbo); } static void opengl_copy_subregion(opengl_info& info, gs_texture_t* source, uint32_t mip_level, uint32_t width, uint32_t height) { GLuint source_ref = *reinterpret_cast(gs_texture_get_obj(source)); // 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() { _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()); throw std::runtime_error("Unexpected error loading required effect file."); } } } 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, mip_generator generator) { { // 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->width() != target->width()) || (source->height() != target->height())) { throw std::invalid_argument("source and target must have same size"); } // Ensure texture formats match if ((source->color_format() != target->color_format())) { throw std::invalid_argument("source and target must have same format"); } } std::string technique = "Linear"; bool separable_technique = false; switch (generator) { case mip_generator::MAGIC_KERNEL_SHARP_PLUS_2021: separable_technique = true; technique = "MagicKernelSharp2021Separable"; break; case mip_generator::MAGIC_KERNEL_2011: separable_technique = false; technique = "MagicKernel2011"; break; case mip_generator::LINEAR: default: break; } // Get a unique lock on the graphics context. auto gctx = streamfx::obs::gs::context(); // Initialize some render targets, and their texture storage. auto rt0 = streamfx::obs::gs::texrender::pool::instance()->acquire(source->color_format(), GS_ZS_NONE); auto rt1 = streamfx::obs::gs::texrender::pool::instance()->acquire(source->color_format(), GS_ZS_NONE); auto rt2 = rt1; if (separable_technique) { rt2 = streamfx::obs::gs::texrender::pool::instance()->acquire(source->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. uint32_t width = source->width(); uint32_t height = source->height(); size_t max_mip_level = calculate_max_mip_level(width, height); { // Copy mip level 0 to output. #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 was_srgb_enabled = gs_framebuffer_srgb_enabled(); bool enable_srgb = gs_get_linear_srgb(); gs_enable_framebuffer_srgb(enable_srgb); // Render each mip level. for (size_t mip = 1; mip < max_mip_level; mip++) { uint32_t cwidth = std::max(width >> mip, 1); uint32_t cheight = std::max(height >> mip, 1); float iwidth = 1.f / static_cast(cwidth); float iheight = 1.f / static_cast(cheight); if (!separable_technique) { try { auto op = rt0->render(cwidth, cheight); gs_ortho(0, 1, 0, 1, 0, 1); if (mip == 1) { _effect.get_parameter("image").set_texture(source, enable_srgb); } else { _effect.get_parameter("image").set_texture(rt1->get_object(), enable_srgb); } _effect.get_parameter("imageTexel").set_float2(iwidth, iheight); _effect.get_parameter("separableTexel").set_float2(iwidth, iheight); while (gs_effect_loop(_effect.get_object(), technique.c_str())) { _gfx_util->draw_fullscreen_triangle(); } } catch (...) { } } else { for (size_t pass = 0; gs_effect_loop(_effect.get_object(), technique.c_str()); pass++) { // Flow: // Mip-Level 1: // - Pass 0: // - Swap rt0, rt2 // - Read from source // - Write to rt0 // - Pass 1: // - Swap rt0, rt2 // - Read from rt2 // - Write to rt0 // - Pass 2: // - Swap rt0, rt2 // - Read from rt2 // - Write to rt0 // - Copy rt0 to target // - Swap rt0, rt1 // // Mip-Level 2: // - Pass 0: // - Swap rt0, rt2 // - Read from rt1 // - Write to rt0 // - (identical to Mip-Level 1) // // This works only if we swap {rt0, rt2} first, otherwise we end up only storing the data from the previous pass. // Swap rt0, rt2. std::swap(rt0, rt2); if (mip == 1) { _effect.get_parameter("image").set_texture(source, enable_srgb); } else if (pass == 0) { _effect.get_parameter("image").set_texture(rt1->get_object(), enable_srgb); } else { _effect.get_parameter("image").set_texture(rt2->get_object(), enable_srgb); } _effect.get_parameter("imageTexel").set_float2(iwidth, iheight); if ((pass % 2) == 0) { _effect.get_parameter("separableTexel").set_float2(iwidth, 0.); } else { _effect.get_parameter("separableTexel").set_float2(0., iheight); } try { auto op = rt0->render(cwidth, cheight); gs_ortho(0, 1, 0, 1, 0, 1); _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, rt0->get_object(), static_cast(mip), cwidth, cheight); } #endif if (gs_get_device_type() == GS_DEVICE_OPENGL) { opengl_copy_subregion(oglinfo, rt0->get_object(), static_cast(mip), cwidth, cheight); } // Swap the render targets. std::swap(rt0, rt1); } // Clean up rendering state. gs_enable_framebuffer_srgb(was_srgb_enabled); gs_blend_state_pop(); // 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 }