diff options
Diffstat (limited to 'packages/kernel/src/rmlui_renderer_gl3.cpp')
| -rw-r--r-- | packages/kernel/src/rmlui_renderer_gl3.cpp | 2197 |
1 files changed, 2197 insertions, 0 deletions
diff --git a/packages/kernel/src/rmlui_renderer_gl3.cpp b/packages/kernel/src/rmlui_renderer_gl3.cpp new file mode 100644 index 0000000..7e192e3 --- /dev/null +++ b/packages/kernel/src/rmlui_renderer_gl3.cpp @@ -0,0 +1,2197 @@ +// Adapted from subprojects/RmlUi-6.2/Backends/RmlUi_Renderer_GL3.{h,cpp} +// (slice-3 spike, prompts/kernel.md item 5). Two deltas from upstream: +// 1. UNBOX_RMLUI_GLES selects the native GLES 3.2 header/shader path +// WITHOUT defining the compiler builtin __ANDROID__ (which would +// poison the whole TU). The branch is otherwise byte-identical to +// upstream's __ANDROID__ branch. +// 2. EndFrame() blits into a settable output framebuffer (default 0) +// instead of the hardcoded default framebuffer, so the bridge can +// direct RmlUi's result into its own offscreen FBO. See +// SetOutputFramebuffer() / output_framebuffer. +// Everything else is verbatim upstream; do not "improve" it. +#include "rmlui_renderer_gl3.h" +#include <RmlUi/Core/Core.h> +#include <RmlUi/Core/DecorationTypes.h> +#include <RmlUi/Core/FileInterface.h> +#include <RmlUi/Core/Geometry.h> +#include <RmlUi/Core/Log.h> +#include <RmlUi/Core/MeshUtilities.h> +#include <RmlUi/Core/Platform.h> +#include <RmlUi/Core/SystemInterface.h> +#include <algorithm> +#include <string.h> + +#if defined RMLUI_PLATFORM_WIN32_NATIVE + // function call missing argument list + #pragma warning(disable : 4551) + // unreferenced local function has been removed + #pragma warning(disable : 4505) +#endif + +#if defined UNBOX_RMLUI_GLES + // unbox: native GLES 3.2 path (HD 4400 / crocus, hardware-verified). + #define RMLUI_SHADER_HEADER_VERSION "#version 320 es\nprecision highp float;\n" + #include <GLES3/gl32.h> +#elif defined RMLUI_PLATFORM_EMSCRIPTEN + #define RMLUI_SHADER_HEADER_VERSION "#version 300 es\nprecision highp float;\n" + #include <GLES3/gl3.h> +#elif defined __ANDROID__ + #define RMLUI_SHADER_HEADER_VERSION "#version 320 es\nprecision highp float;\n" + #include <GLES3/gl32.h> +#elif defined RMLUI_GL3_CUSTOM_LOADER + #define RMLUI_SHADER_HEADER_VERSION "#version 330\n" + #include RMLUI_GL3_CUSTOM_LOADER +#else + #define RMLUI_SHADER_HEADER_VERSION "#version 330\n" + #define GLAD_GL_IMPLEMENTATION + #include "RmlUi_Include_GL3.h" +#endif + +// Determines the anti-aliasing quality when creating layers. Enables better-looking visuals, especially when transforms are applied. +#ifndef RMLUI_NUM_MSAA_SAMPLES + #define RMLUI_NUM_MSAA_SAMPLES 2 +#endif + +#define MAX_NUM_STOPS 16 +#define BLUR_SIZE 7 +#define BLUR_NUM_WEIGHTS ((BLUR_SIZE + 1) / 2) + +#define RMLUI_STRINGIFY_IMPL(x) #x +#define RMLUI_STRINGIFY(x) RMLUI_STRINGIFY_IMPL(x) + +#define RMLUI_SHADER_HEADER \ + RMLUI_SHADER_HEADER_VERSION "#define MAX_NUM_STOPS " RMLUI_STRINGIFY(MAX_NUM_STOPS) "\n#line " RMLUI_STRINGIFY(__LINE__) "\n" + +static const char* shader_vert_main = RMLUI_SHADER_HEADER R"( +uniform vec2 _translate; +uniform mat4 _transform; + +in vec2 inPosition; +in vec4 inColor0; +in vec2 inTexCoord0; + +out vec2 fragTexCoord; +out vec4 fragColor; + +void main() { + fragTexCoord = inTexCoord0; + fragColor = inColor0; + + vec2 translatedPos = inPosition + _translate; + vec4 outPos = _transform * vec4(translatedPos, 0.0, 1.0); + + gl_Position = outPos; +} +)"; +static const char* shader_frag_texture = RMLUI_SHADER_HEADER R"( +uniform sampler2D _tex; +in vec2 fragTexCoord; +in vec4 fragColor; + +out vec4 finalColor; + +void main() { + vec4 texColor = texture(_tex, fragTexCoord); + finalColor = fragColor * texColor; +} +)"; +static const char* shader_frag_color = RMLUI_SHADER_HEADER R"( +in vec2 fragTexCoord; +in vec4 fragColor; + +out vec4 finalColor; + +void main() { + finalColor = fragColor; +} +)"; + +enum class ShaderGradientFunction { Linear, Radial, Conic, RepeatingLinear, RepeatingRadial, RepeatingConic }; // Must match shader definitions below. + +static const char* shader_frag_gradient = RMLUI_SHADER_HEADER R"( +#define LINEAR 0 +#define RADIAL 1 +#define CONIC 2 +#define REPEATING_LINEAR 3 +#define REPEATING_RADIAL 4 +#define REPEATING_CONIC 5 +#define PI 3.14159265 + +uniform int _func; // one of the above definitions +uniform vec2 _p; // linear: starting point, radial: center, conic: center +uniform vec2 _v; // linear: vector to ending point, radial: 2d curvature (inverse radius), conic: angled unit vector +uniform vec4 _stop_colors[MAX_NUM_STOPS]; +uniform float _stop_positions[MAX_NUM_STOPS]; // normalized, 0 -> starting point, 1 -> ending point +uniform int _num_stops; + +in vec2 fragTexCoord; +in vec4 fragColor; +out vec4 finalColor; + +vec4 mix_stop_colors(float t) { + vec4 color = _stop_colors[0]; + + for (int i = 1; i < _num_stops; i++) + color = mix(color, _stop_colors[i], smoothstep(_stop_positions[i-1], _stop_positions[i], t)); + + return color; +} + +void main() { + float t = 0.0; + + if (_func == LINEAR || _func == REPEATING_LINEAR) + { + float dist_square = dot(_v, _v); + vec2 V = fragTexCoord - _p; + t = dot(_v, V) / dist_square; + } + else if (_func == RADIAL || _func == REPEATING_RADIAL) + { + vec2 V = fragTexCoord - _p; + t = length(_v * V); + } + else if (_func == CONIC || _func == REPEATING_CONIC) + { + mat2 R = mat2(_v.x, -_v.y, _v.y, _v.x); + vec2 V = R * (fragTexCoord - _p); + t = 0.5 + atan(-V.x, V.y) / (2.0 * PI); + } + + if (_func == REPEATING_LINEAR || _func == REPEATING_RADIAL || _func == REPEATING_CONIC) + { + float t0 = _stop_positions[0]; + float t1 = _stop_positions[_num_stops - 1]; + t = t0 + mod(t - t0, t1 - t0); + } + + finalColor = fragColor * mix_stop_colors(t); +} +)"; + +// "Creation" by Danilo Guanabara, based on: https://www.shadertoy.com/view/XsXXDn +static const char* shader_frag_creation = RMLUI_SHADER_HEADER R"( +uniform float _value; +uniform vec2 _dimensions; + +in vec2 fragTexCoord; +in vec4 fragColor; +out vec4 finalColor; + +void main() { + float t = _value; + vec3 c; + float l; + for (int i = 0; i < 3; i++) { + vec2 p = fragTexCoord; + vec2 uv = p; + p -= .5; + p.x *= _dimensions.x / _dimensions.y; + float z = t + float(i) * .07; + l = length(p); + uv += p / l * (sin(z) + 1.) * abs(sin(l * 9. - z - z)); + c[i] = .01 / length(mod(uv, 1.) - .5); + } + finalColor = vec4(c / l, fragColor.a); +} +)"; + +static const char* shader_vert_passthrough = RMLUI_SHADER_HEADER R"( +in vec2 inPosition; +in vec2 inTexCoord0; + +out vec2 fragTexCoord; + +void main() { + fragTexCoord = inTexCoord0; + gl_Position = vec4(inPosition, 0.0, 1.0); +} +)"; +static const char* shader_frag_passthrough = RMLUI_SHADER_HEADER R"( +uniform sampler2D _tex; +in vec2 fragTexCoord; +out vec4 finalColor; + +void main() { + finalColor = texture(_tex, fragTexCoord); +} +)"; +static const char* shader_frag_color_matrix = RMLUI_SHADER_HEADER R"( +uniform sampler2D _tex; +uniform mat4 _color_matrix; + +in vec2 fragTexCoord; +out vec4 finalColor; + +void main() { + // The general case uses a 4x5 color matrix for full rgba transformation, plus a constant term with the last column. + // However, we only consider the case of rgb transformations. Thus, we could in principle use a 3x4 matrix, but we + // keep the alpha row for simplicity. + // In the general case we should do the matrix transformation in non-premultiplied space. However, without alpha + // transformations, we can do it directly in premultiplied space to avoid the extra division and multiplication + // steps. In this space, the constant term needs to be multiplied by the alpha value, instead of unity. + vec4 texColor = texture(_tex, fragTexCoord); + vec3 transformedColor = vec3(_color_matrix * texColor); + finalColor = vec4(transformedColor, texColor.a); +} +)"; +static const char* shader_frag_blend_mask = RMLUI_SHADER_HEADER R"( +uniform sampler2D _tex; +uniform sampler2D _texMask; + +in vec2 fragTexCoord; +out vec4 finalColor; + +void main() { + vec4 texColor = texture(_tex, fragTexCoord); + float maskAlpha = texture(_texMask, fragTexCoord).a; + finalColor = texColor * maskAlpha; +} +)"; + +#define RMLUI_SHADER_BLUR_HEADER \ + RMLUI_SHADER_HEADER "\n#define BLUR_SIZE " RMLUI_STRINGIFY(BLUR_SIZE) "\n#define BLUR_NUM_WEIGHTS " RMLUI_STRINGIFY(BLUR_NUM_WEIGHTS) + +static const char* shader_vert_blur = RMLUI_SHADER_BLUR_HEADER R"( +uniform vec2 _texelOffset; + +in vec3 inPosition; +in vec2 inTexCoord0; + +out vec2 fragTexCoord[BLUR_SIZE]; + +void main() { + for(int i = 0; i < BLUR_SIZE; i++) + fragTexCoord[i] = inTexCoord0 - float(i - BLUR_NUM_WEIGHTS + 1) * _texelOffset; + gl_Position = vec4(inPosition, 1.0); +} +)"; +static const char* shader_frag_blur = RMLUI_SHADER_BLUR_HEADER R"( +uniform sampler2D _tex; +uniform float _weights[BLUR_NUM_WEIGHTS]; +uniform vec2 _texCoordMin; +uniform vec2 _texCoordMax; + +in vec2 fragTexCoord[BLUR_SIZE]; +out vec4 finalColor; + +void main() { + vec4 color = vec4(0.0, 0.0, 0.0, 0.0); + for(int i = 0; i < BLUR_SIZE; i++) + { + vec2 in_region = step(_texCoordMin, fragTexCoord[i]) * step(fragTexCoord[i], _texCoordMax); + color += texture(_tex, fragTexCoord[i]) * in_region.x * in_region.y * _weights[abs(i - BLUR_NUM_WEIGHTS + 1)]; + } + finalColor = color; +} +)"; +static const char* shader_frag_drop_shadow = RMLUI_SHADER_HEADER R"( +uniform sampler2D _tex; +uniform vec2 _texCoordMin; +uniform vec2 _texCoordMax; +uniform vec4 _color; + +in vec2 fragTexCoord; +out vec4 finalColor; + +void main() { + vec2 in_region = step(_texCoordMin, fragTexCoord) * step(fragTexCoord, _texCoordMax); + finalColor = texture(_tex, fragTexCoord).a * in_region.x * in_region.y * _color; +} +)"; + +enum class ProgramId { + None, + Color, + Texture, + Gradient, + Creation, + Passthrough, + ColorMatrix, + BlendMask, + Blur, + DropShadow, + Count, +}; +enum class VertShaderId { + Main, + Passthrough, + Blur, + Count, +}; +enum class FragShaderId { + Color, + Texture, + Gradient, + Creation, + Passthrough, + ColorMatrix, + BlendMask, + Blur, + DropShadow, + Count, +}; +enum class UniformId { + Translate, + Transform, + Tex, + Color, + ColorMatrix, + TexelOffset, + TexCoordMin, + TexCoordMax, + TexMask, + Weights, + Func, + P, + V, + StopColors, + StopPositions, + NumStops, + Value, + Dimensions, + Count, +}; + +namespace Gfx { + +static const char* const program_uniform_names[(size_t)UniformId::Count] = {"_translate", "_transform", "_tex", "_color", "_color_matrix", + "_texelOffset", "_texCoordMin", "_texCoordMax", "_texMask", "_weights[0]", "_func", "_p", "_v", "_stop_colors[0]", "_stop_positions[0]", + "_num_stops", "_value", "_dimensions"}; + +enum class VertexAttribute { Position, Color0, TexCoord0, Count }; +static const char* const vertex_attribute_names[(size_t)VertexAttribute::Count] = {"inPosition", "inColor0", "inTexCoord0"}; + +struct VertShaderDefinition { + VertShaderId id; + const char* name_str; + const char* code_str; +}; +struct FragShaderDefinition { + FragShaderId id; + const char* name_str; + const char* code_str; +}; +struct ProgramDefinition { + ProgramId id; + const char* name_str; + VertShaderId vert_shader; + FragShaderId frag_shader; +}; + +// clang-format off +static const VertShaderDefinition vert_shader_definitions[] = { + {VertShaderId::Main, "main", shader_vert_main}, + {VertShaderId::Passthrough, "passthrough", shader_vert_passthrough}, + {VertShaderId::Blur, "blur", shader_vert_blur}, +}; +static const FragShaderDefinition frag_shader_definitions[] = { + {FragShaderId::Color, "color", shader_frag_color}, + {FragShaderId::Texture, "texture", shader_frag_texture}, + {FragShaderId::Gradient, "gradient", shader_frag_gradient}, + {FragShaderId::Creation, "creation", shader_frag_creation}, + {FragShaderId::Passthrough, "passthrough", shader_frag_passthrough}, + {FragShaderId::ColorMatrix, "color_matrix", shader_frag_color_matrix}, + {FragShaderId::BlendMask, "blend_mask", shader_frag_blend_mask}, + {FragShaderId::Blur, "blur", shader_frag_blur}, + {FragShaderId::DropShadow, "drop_shadow", shader_frag_drop_shadow}, +}; +static const ProgramDefinition program_definitions[] = { + {ProgramId::Color, "color", VertShaderId::Main, FragShaderId::Color}, + {ProgramId::Texture, "texture", VertShaderId::Main, FragShaderId::Texture}, + {ProgramId::Gradient, "gradient", VertShaderId::Main, FragShaderId::Gradient}, + {ProgramId::Creation, "creation", VertShaderId::Main, FragShaderId::Creation}, + {ProgramId::Passthrough, "passthrough", VertShaderId::Passthrough, FragShaderId::Passthrough}, + {ProgramId::ColorMatrix, "color_matrix", VertShaderId::Passthrough, FragShaderId::ColorMatrix}, + {ProgramId::BlendMask, "blend_mask", VertShaderId::Passthrough, FragShaderId::BlendMask}, + {ProgramId::Blur, "blur", VertShaderId::Blur, FragShaderId::Blur}, + {ProgramId::DropShadow, "drop_shadow", VertShaderId::Passthrough, FragShaderId::DropShadow}, +}; +// clang-format on + +template <typename T, typename Enum> +class EnumArray { +public: + const T& operator[](Enum id) const + { + RMLUI_ASSERT((size_t)id < (size_t)Enum::Count); + return ids[size_t(id)]; + } + T& operator[](Enum id) + { + RMLUI_ASSERT((size_t)id < (size_t)Enum::Count); + return ids[size_t(id)]; + } + auto begin() const { return ids.begin(); } + auto end() const { return ids.end(); } + +private: + Rml::Array<T, (size_t)Enum::Count> ids = {}; +}; + +using Programs = EnumArray<GLuint, ProgramId>; +using VertShaders = EnumArray<GLuint, VertShaderId>; +using FragShaders = EnumArray<GLuint, FragShaderId>; + +class Uniforms { +public: + GLint Get(ProgramId id, UniformId uniform) const + { + auto it = map.find(ToKey(id, uniform)); + if (it != map.end()) + return it->second; + return -1; + } + void Insert(ProgramId id, UniformId uniform, GLint location) { map[ToKey(id, uniform)] = location; } + +private: + using Key = uint64_t; + Key ToKey(ProgramId id, UniformId uniform) const { return (static_cast<Key>(id) << 32) | static_cast<Key>(uniform); } + Rml::UnorderedMap<Key, GLint> map; +}; + +struct ProgramData { + Programs programs; + VertShaders vert_shaders; + FragShaders frag_shaders; + Uniforms uniforms; +}; + +struct CompiledGeometryData { + GLuint vao; + GLuint vbo; + GLuint ibo; + GLsizei draw_count; +}; + +struct FramebufferData { + int width, height; + GLuint framebuffer; + GLuint color_tex_buffer; + GLuint color_render_buffer; + GLuint depth_stencil_buffer; + bool owns_depth_stencil_buffer; +}; + +enum class FramebufferAttachment { None, DepthStencil }; + +static void CheckGLError(const char* operation_name) +{ +#ifdef RMLUI_DEBUG + GLenum error_code = glGetError(); + if (error_code != GL_NO_ERROR) + { + static const Rml::Pair<GLenum, const char*> error_names[] = {{GL_INVALID_ENUM, "GL_INVALID_ENUM"}, {GL_INVALID_VALUE, "GL_INVALID_VALUE"}, + {GL_INVALID_OPERATION, "GL_INVALID_OPERATION"}, {GL_OUT_OF_MEMORY, "GL_OUT_OF_MEMORY"}}; + const char* error_str = "''"; + for (auto& err : error_names) + { + if (err.first == error_code) + { + error_str = err.second; + break; + } + } + Rml::Log::Message(Rml::Log::LT_ERROR, "OpenGL error during %s. Error code 0x%x (%s).", operation_name, error_code, error_str); + } +#endif + (void)operation_name; +} + +// Create the shader, 'shader_type' is either GL_VERTEX_SHADER or GL_FRAGMENT_SHADER. +static bool CreateShader(GLuint& out_shader_id, GLenum shader_type, const char* code_string) +{ + RMLUI_ASSERT(shader_type == GL_VERTEX_SHADER || shader_type == GL_FRAGMENT_SHADER); + + GLuint id = glCreateShader(shader_type); + glShaderSource(id, 1, (const GLchar**)&code_string, NULL); + glCompileShader(id); + + GLint status = 0; + glGetShaderiv(id, GL_COMPILE_STATUS, &status); + if (status == GL_FALSE) + { + GLint info_log_length = 0; + glGetShaderiv(id, GL_INFO_LOG_LENGTH, &info_log_length); + char* info_log_string = new char[info_log_length + 1]; + glGetShaderInfoLog(id, info_log_length, NULL, info_log_string); + + Rml::Log::Message(Rml::Log::LT_ERROR, "Compile failure in OpenGL shader: %s", info_log_string); + delete[] info_log_string; + glDeleteShader(id); + return false; + } + + CheckGLError("CreateShader"); + + out_shader_id = id; + return true; +} + +static bool CreateProgram(GLuint& out_program, Uniforms& inout_uniform_map, ProgramId program_id, GLuint vertex_shader, GLuint fragment_shader) +{ + GLuint id = glCreateProgram(); + RMLUI_ASSERT(id); + + for (GLuint i = 0; i < (GLuint)VertexAttribute::Count; i++) + glBindAttribLocation(id, i, vertex_attribute_names[i]); + + CheckGLError("BindAttribLocations"); + + glAttachShader(id, vertex_shader); + glAttachShader(id, fragment_shader); + + glLinkProgram(id); + + glDetachShader(id, vertex_shader); + glDetachShader(id, fragment_shader); + + GLint status = 0; + glGetProgramiv(id, GL_LINK_STATUS, &status); + if (status == GL_FALSE) + { + GLint info_log_length = 0; + glGetProgramiv(id, GL_INFO_LOG_LENGTH, &info_log_length); + char* info_log_string = new char[info_log_length + 1]; + glGetProgramInfoLog(id, info_log_length, NULL, info_log_string); + + Rml::Log::Message(Rml::Log::LT_ERROR, "OpenGL program linking failure: %s", info_log_string); + delete[] info_log_string; + glDeleteProgram(id); + return false; + } + + out_program = id; + + // Make a lookup table for the uniform locations. + GLint num_active_uniforms = 0; + glGetProgramiv(id, GL_ACTIVE_UNIFORMS, &num_active_uniforms); + + constexpr size_t name_size = 64; + GLchar name_buf[name_size] = ""; + for (int unif = 0; unif < num_active_uniforms; ++unif) + { + GLint array_size = 0; + GLenum type = 0; + GLsizei actual_length = 0; + glGetActiveUniform(id, unif, name_size, &actual_length, &array_size, &type, name_buf); + GLint location = glGetUniformLocation(id, name_buf); + + // See if we have the name in our pre-defined name list. + UniformId program_uniform = UniformId::Count; + for (int i = 0; i < (int)UniformId::Count; i++) + { + const char* uniform_name = program_uniform_names[i]; + if (strcmp(name_buf, uniform_name) == 0) + { + program_uniform = (UniformId)i; + break; + } + } + + if ((size_t)program_uniform < (size_t)UniformId::Count) + { + inout_uniform_map.Insert(program_id, program_uniform, location); + } + else + { + Rml::Log::Message(Rml::Log::LT_ERROR, "OpenGL program uses unknown uniform '%s'.", name_buf); + return false; + } + } + + CheckGLError("CreateProgram"); + + return true; +} + +static bool CreateFramebuffer(FramebufferData& out_fb, int width, int height, int samples, FramebufferAttachment attachment, + GLuint shared_depth_stencil_buffer) +{ +#if defined(UNBOX_RMLUI_GLES) || defined(RMLUI_PLATFORM_EMSCRIPTEN) || defined(__ANDROID__) + constexpr GLint wrap_mode = GL_CLAMP_TO_EDGE; +#else + constexpr GLint wrap_mode = GL_CLAMP_TO_BORDER; // GL_REPEAT GL_MIRRORED_REPEAT GL_CLAMP_TO_EDGE +#endif + + constexpr GLenum color_format = GL_RGBA8; // GL_RGBA8 GL_SRGB8_ALPHA8 GL_RGBA16F + constexpr GLint min_mag_filter = GL_LINEAR; // GL_NEAREST + const Rml::Colourf border_color(0.f, 0.f); + + GLuint framebuffer = 0; + glGenFramebuffers(1, &framebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + + GLuint color_tex_buffer = 0; + GLuint color_render_buffer = 0; + if (samples > 0) + { + glGenRenderbuffers(1, &color_render_buffer); + glBindRenderbuffer(GL_RENDERBUFFER, color_render_buffer); + glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, color_format, width, height); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, color_render_buffer); + } + else + { + glGenTextures(1, &color_tex_buffer); + glBindTexture(GL_TEXTURE_2D, color_tex_buffer); + glTexImage2D(GL_TEXTURE_2D, 0, color_format, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_mag_filter); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, min_mag_filter); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap_mode); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap_mode); +#if !defined(UNBOX_RMLUI_GLES) && !defined(RMLUI_PLATFORM_EMSCRIPTEN) && !defined(__ANDROID__) + glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, &border_color[0]); +#endif + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color_tex_buffer, 0); + } + + // Create depth/stencil buffer storage attachment. + GLuint depth_stencil_buffer = 0; + if (attachment != FramebufferAttachment::None) + { + if (shared_depth_stencil_buffer) + { + // Share depth/stencil buffer + depth_stencil_buffer = shared_depth_stencil_buffer; + } + else + { + // Create new depth/stencil buffer + glGenRenderbuffers(1, &depth_stencil_buffer); + glBindRenderbuffer(GL_RENDERBUFFER, depth_stencil_buffer); + + glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_DEPTH24_STENCIL8, width, height); + } + + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depth_stencil_buffer); + } + + const GLuint framebuffer_status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (framebuffer_status != GL_FRAMEBUFFER_COMPLETE) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "OpenGL framebuffer could not be generated. Error code %x.", framebuffer_status); + return false; + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glBindTexture(GL_TEXTURE_2D, 0); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + CheckGLError("CreateFramebuffer"); + + out_fb = {}; + out_fb.width = width; + out_fb.height = height; + out_fb.framebuffer = framebuffer; + out_fb.color_tex_buffer = color_tex_buffer; + out_fb.color_render_buffer = color_render_buffer; + out_fb.depth_stencil_buffer = depth_stencil_buffer; + out_fb.owns_depth_stencil_buffer = !shared_depth_stencil_buffer; + + return true; +} + +static void DestroyFramebuffer(FramebufferData& fb) +{ + if (fb.framebuffer) + glDeleteFramebuffers(1, &fb.framebuffer); + if (fb.color_tex_buffer) + glDeleteTextures(1, &fb.color_tex_buffer); + if (fb.color_render_buffer) + glDeleteRenderbuffers(1, &fb.color_render_buffer); + if (fb.owns_depth_stencil_buffer && fb.depth_stencil_buffer) + glDeleteRenderbuffers(1, &fb.depth_stencil_buffer); + fb = {}; +} + +static GLuint CreateTexture(Rml::Span<const Rml::byte> source_data, Rml::Vector2i source_dimensions) +{ + GLuint texture_id = 0; + glGenTextures(1, &texture_id); + if (texture_id == 0) + return 0; + + glBindTexture(GL_TEXTURE_2D, texture_id); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, source_dimensions.x, source_dimensions.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, source_data.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + + glBindTexture(GL_TEXTURE_2D, 0); + + return texture_id; +} + +static void BindTexture(const FramebufferData& fb) +{ + if (!fb.color_tex_buffer) + { + RMLUI_ERRORMSG("Only framebuffers with color textures can be bound as textures. This framebuffer probably uses multisampling which needs a " + "blit step first."); + } + + glBindTexture(GL_TEXTURE_2D, fb.color_tex_buffer); +} + +static bool CreateShaders(ProgramData& data) +{ + RMLUI_ASSERT(std::all_of(data.vert_shaders.begin(), data.vert_shaders.end(), [](auto&& value) { return value == 0; })); + RMLUI_ASSERT(std::all_of(data.frag_shaders.begin(), data.frag_shaders.end(), [](auto&& value) { return value == 0; })); + RMLUI_ASSERT(std::all_of(data.programs.begin(), data.programs.end(), [](auto&& value) { return value == 0; })); + auto ReportError = [](const char* type, const char* name) { + Rml::Log::Message(Rml::Log::LT_ERROR, "Could not create OpenGL %s: '%s'.", type, name); + return false; + }; + + for (const VertShaderDefinition& def : vert_shader_definitions) + { + if (!CreateShader(data.vert_shaders[def.id], GL_VERTEX_SHADER, def.code_str)) + return ReportError("vertex shader", def.name_str); + } + + for (const FragShaderDefinition& def : frag_shader_definitions) + { + if (!CreateShader(data.frag_shaders[def.id], GL_FRAGMENT_SHADER, def.code_str)) + return ReportError("fragment shader", def.name_str); + } + + for (const ProgramDefinition& def : program_definitions) + { + if (!CreateProgram(data.programs[def.id], data.uniforms, def.id, data.vert_shaders[def.vert_shader], data.frag_shaders[def.frag_shader])) + return ReportError("program", def.name_str); + } + + glUseProgram(data.programs[ProgramId::BlendMask]); + glUniform1i(data.uniforms.Get(ProgramId::BlendMask, UniformId::TexMask), 1); + + glUseProgram(0); + + return true; +} + +static void DestroyShaders(const ProgramData& data) +{ + for (GLuint id : data.programs) + glDeleteProgram(id); + + for (GLuint id : data.vert_shaders) + glDeleteShader(id); + + for (GLuint id : data.frag_shaders) + glDeleteShader(id); +} + +} // namespace Gfx + +RenderInterface_GL3::RenderInterface_GL3() +{ + auto mut_program_data = Rml::MakeUnique<Gfx::ProgramData>(); + if (Gfx::CreateShaders(*mut_program_data)) + { + program_data = std::move(mut_program_data); + Rml::Mesh mesh; + Rml::MeshUtilities::GenerateQuad(mesh, Rml::Vector2f(-1), Rml::Vector2f(2), {}); + fullscreen_quad_geometry = RenderInterface_GL3::CompileGeometry(mesh.vertices, mesh.indices); + } +} + +RenderInterface_GL3::~RenderInterface_GL3() +{ + if (fullscreen_quad_geometry) + { + RenderInterface_GL3::ReleaseGeometry(fullscreen_quad_geometry); + fullscreen_quad_geometry = {}; + } + + if (program_data) + { + Gfx::DestroyShaders(*program_data); + program_data.reset(); + } +} + +void RenderInterface_GL3::SetViewport(int width, int height, int offset_x, int offset_y) +{ + viewport_width = Rml::Math::Max(width, 1); + viewport_height = Rml::Math::Max(height, 1); + viewport_offset_x = offset_x; + viewport_offset_y = offset_y; + projection = Rml::Matrix4f::ProjectOrtho(0, (float)viewport_width, (float)viewport_height, 0, -10000, 10000); +} + +void RenderInterface_GL3::BeginFrame() +{ + RMLUI_ASSERT(viewport_width >= 1 && viewport_height >= 1); + + // Backup GL state. + glstate_backup.enable_cull_face = glIsEnabled(GL_CULL_FACE); + glstate_backup.enable_blend = glIsEnabled(GL_BLEND); + glstate_backup.enable_stencil_test = glIsEnabled(GL_STENCIL_TEST); + glstate_backup.enable_scissor_test = glIsEnabled(GL_SCISSOR_TEST); + glstate_backup.enable_depth_test = glIsEnabled(GL_DEPTH_TEST); + + glGetIntegerv(GL_VIEWPORT, glstate_backup.viewport); + glGetIntegerv(GL_SCISSOR_BOX, glstate_backup.scissor); + + glGetIntegerv(GL_ACTIVE_TEXTURE, &glstate_backup.active_texture); + + glGetIntegerv(GL_STENCIL_CLEAR_VALUE, &glstate_backup.stencil_clear_value); + glGetFloatv(GL_COLOR_CLEAR_VALUE, glstate_backup.color_clear_value); + glGetBooleanv(GL_COLOR_WRITEMASK, glstate_backup.color_writemask); + + glGetIntegerv(GL_BLEND_EQUATION_RGB, &glstate_backup.blend_equation_rgb); + glGetIntegerv(GL_BLEND_EQUATION_ALPHA, &glstate_backup.blend_equation_alpha); + glGetIntegerv(GL_BLEND_SRC_RGB, &glstate_backup.blend_src_rgb); + glGetIntegerv(GL_BLEND_DST_RGB, &glstate_backup.blend_dst_rgb); + glGetIntegerv(GL_BLEND_SRC_ALPHA, &glstate_backup.blend_src_alpha); + glGetIntegerv(GL_BLEND_DST_ALPHA, &glstate_backup.blend_dst_alpha); + + glGetIntegerv(GL_STENCIL_FUNC, &glstate_backup.stencil_front.func); + glGetIntegerv(GL_STENCIL_REF, &glstate_backup.stencil_front.ref); + glGetIntegerv(GL_STENCIL_VALUE_MASK, &glstate_backup.stencil_front.value_mask); + glGetIntegerv(GL_STENCIL_WRITEMASK, &glstate_backup.stencil_front.writemask); + glGetIntegerv(GL_STENCIL_FAIL, &glstate_backup.stencil_front.fail); + glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &glstate_backup.stencil_front.pass_depth_fail); + glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &glstate_backup.stencil_front.pass_depth_pass); + + glGetIntegerv(GL_STENCIL_BACK_FUNC, &glstate_backup.stencil_back.func); + glGetIntegerv(GL_STENCIL_BACK_REF, &glstate_backup.stencil_back.ref); + glGetIntegerv(GL_STENCIL_BACK_VALUE_MASK, &glstate_backup.stencil_back.value_mask); + glGetIntegerv(GL_STENCIL_BACK_WRITEMASK, &glstate_backup.stencil_back.writemask); + glGetIntegerv(GL_STENCIL_BACK_FAIL, &glstate_backup.stencil_back.fail); + glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_FAIL, &glstate_backup.stencil_back.pass_depth_fail); + glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_PASS, &glstate_backup.stencil_back.pass_depth_pass); + + // Setup expected GL state. + glViewport(0, 0, viewport_width, viewport_height); + + glClearStencil(0); + glClearColor(0, 0, 0, 0); + + glActiveTexture(GL_TEXTURE0); + + glDisable(GL_SCISSOR_TEST); + glDisable(GL_CULL_FACE); + + // Set blending function for premultiplied alpha. + glEnable(GL_BLEND); + glBlendEquation(GL_FUNC_ADD); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + +#if !defined(UNBOX_RMLUI_GLES) && !defined(RMLUI_PLATFORM_EMSCRIPTEN) && !defined(__ANDROID__) + // We do blending in nonlinear sRGB space because that is the common practice and gives results that we are used to. + glDisable(GL_FRAMEBUFFER_SRGB); +#endif + + glEnable(GL_STENCIL_TEST); + glStencilFunc(GL_ALWAYS, 1, GLuint(-1)); + glStencilMask(GLuint(-1)); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + + glDisable(GL_DEPTH_TEST); + + SetTransform(nullptr); + + render_layers.BeginFrame(viewport_width, viewport_height); + glBindFramebuffer(GL_FRAMEBUFFER, render_layers.GetTopLayer().framebuffer); + glClear(GL_COLOR_BUFFER_BIT); + + UseProgram(ProgramId::None); + program_transform_dirty.set(); + scissor_state = Rml::Rectanglei::MakeInvalid(); + + Gfx::CheckGLError("BeginFrame"); +} + +void RenderInterface_GL3::EndFrame() +{ + const Gfx::FramebufferData& fb_active = render_layers.GetTopLayer(); + const Gfx::FramebufferData& fb_postprocess = render_layers.GetPostprocessPrimary(); + + // Resolve MSAA to postprocess framebuffer. + glBindFramebuffer(GL_READ_FRAMEBUFFER, fb_active.framebuffer); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fb_postprocess.framebuffer); + + glBlitFramebuffer(0, 0, fb_active.width, fb_active.height, 0, 0, fb_postprocess.width, fb_postprocess.height, GL_COLOR_BUFFER_BIT, GL_NEAREST); + + // Draw to the output framebuffer (unbox: default 0 = backbuffer, but the + // spike bridge redirects this to its own offscreen FBO). + glBindFramebuffer(GL_FRAMEBUFFER, output_framebuffer); + glViewport(viewport_offset_x, viewport_offset_y, viewport_width, viewport_height); + + // Assuming we have an opaque background, we can just write to it with the premultiplied alpha blend mode and we'll get the correct result. + // Instead, if we had a transparent destination that didn't use premultiplied alpha, we would need to perform a manual un-premultiplication step. + glActiveTexture(GL_TEXTURE0); + Gfx::BindTexture(fb_postprocess); + UseProgram(ProgramId::Passthrough); + if (output_flip_y) + // unbox: the spike's offscreen FBO is read out / scanned out with row + // 0 = top (wlr_buffer convention), but GL's framebuffer origin is + // bottom-left. Flip V on the final composite so the submitted buffer + // is upright. Display and document/input coords then match 1:1. + DrawFullscreenQuad(Rml::Vector2f(0.f, 1.f), Rml::Vector2f(1.f, -1.f)); + else + DrawFullscreenQuad(); + + render_layers.EndFrame(); + + // Restore GL state. + if (glstate_backup.enable_cull_face) + glEnable(GL_CULL_FACE); + else + glDisable(GL_CULL_FACE); + + if (glstate_backup.enable_blend) + glEnable(GL_BLEND); + else + glDisable(GL_BLEND); + + if (glstate_backup.enable_stencil_test) + glEnable(GL_STENCIL_TEST); + else + glDisable(GL_STENCIL_TEST); + + if (glstate_backup.enable_scissor_test) + glEnable(GL_SCISSOR_TEST); + else + glDisable(GL_SCISSOR_TEST); + + if (glstate_backup.enable_depth_test) + glEnable(GL_DEPTH_TEST); + else + glDisable(GL_DEPTH_TEST); + + glViewport(glstate_backup.viewport[0], glstate_backup.viewport[1], glstate_backup.viewport[2], glstate_backup.viewport[3]); + glScissor(glstate_backup.scissor[0], glstate_backup.scissor[1], glstate_backup.scissor[2], glstate_backup.scissor[3]); + + glActiveTexture(glstate_backup.active_texture); + + glClearStencil(glstate_backup.stencil_clear_value); + glClearColor(glstate_backup.color_clear_value[0], glstate_backup.color_clear_value[1], glstate_backup.color_clear_value[2], + glstate_backup.color_clear_value[3]); + glColorMask(glstate_backup.color_writemask[0], glstate_backup.color_writemask[1], glstate_backup.color_writemask[2], + glstate_backup.color_writemask[3]); + + glBlendEquationSeparate(glstate_backup.blend_equation_rgb, glstate_backup.blend_equation_alpha); + glBlendFuncSeparate(glstate_backup.blend_src_rgb, glstate_backup.blend_dst_rgb, glstate_backup.blend_src_alpha, glstate_backup.blend_dst_alpha); + + glStencilFuncSeparate(GL_FRONT, glstate_backup.stencil_front.func, glstate_backup.stencil_front.ref, glstate_backup.stencil_front.value_mask); + glStencilMaskSeparate(GL_FRONT, glstate_backup.stencil_front.writemask); + glStencilOpSeparate(GL_FRONT, glstate_backup.stencil_front.fail, glstate_backup.stencil_front.pass_depth_fail, + glstate_backup.stencil_front.pass_depth_pass); + + glStencilFuncSeparate(GL_BACK, glstate_backup.stencil_back.func, glstate_backup.stencil_back.ref, glstate_backup.stencil_back.value_mask); + glStencilMaskSeparate(GL_BACK, glstate_backup.stencil_back.writemask); + glStencilOpSeparate(GL_BACK, glstate_backup.stencil_back.fail, glstate_backup.stencil_back.pass_depth_fail, + glstate_backup.stencil_back.pass_depth_pass); + + Gfx::CheckGLError("EndFrame"); +} + +void RenderInterface_GL3::Clear() +{ + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); +} + +Rml::CompiledGeometryHandle RenderInterface_GL3::CompileGeometry(Rml::Span<const Rml::Vertex> vertices, Rml::Span<const int> indices) +{ + constexpr GLenum draw_usage = GL_STATIC_DRAW; + + GLuint vao = 0; + GLuint vbo = 0; + GLuint ibo = 0; + + glGenVertexArrays(1, &vao); + glGenBuffers(1, &vbo); + glGenBuffers(1, &ibo); + glBindVertexArray(vao); + + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(Rml::Vertex) * vertices.size(), (const void*)vertices.data(), draw_usage); + + glEnableVertexAttribArray((GLuint)Gfx::VertexAttribute::Position); + glVertexAttribPointer((GLuint)Gfx::VertexAttribute::Position, 2, GL_FLOAT, GL_FALSE, sizeof(Rml::Vertex), + (const GLvoid*)(offsetof(Rml::Vertex, position))); + + glEnableVertexAttribArray((GLuint)Gfx::VertexAttribute::Color0); + glVertexAttribPointer((GLuint)Gfx::VertexAttribute::Color0, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Rml::Vertex), + (const GLvoid*)(offsetof(Rml::Vertex, colour))); + + glEnableVertexAttribArray((GLuint)Gfx::VertexAttribute::TexCoord0); + glVertexAttribPointer((GLuint)Gfx::VertexAttribute::TexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(Rml::Vertex), + (const GLvoid*)(offsetof(Rml::Vertex, tex_coord))); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(int) * indices.size(), (const void*)indices.data(), draw_usage); + + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + Gfx::CheckGLError("CompileGeometry"); + + Gfx::CompiledGeometryData* geometry = new Gfx::CompiledGeometryData; + geometry->vao = vao; + geometry->vbo = vbo; + geometry->ibo = ibo; + geometry->draw_count = (GLsizei)indices.size(); + + return (Rml::CompiledGeometryHandle)geometry; +} + +void RenderInterface_GL3::RenderGeometry(Rml::CompiledGeometryHandle handle, Rml::Vector2f translation, Rml::TextureHandle texture) +{ + Gfx::CompiledGeometryData* geometry = (Gfx::CompiledGeometryData*)handle; + + if (texture == TexturePostprocess) + { + // Do nothing. + } + else if (texture) + { + UseProgram(ProgramId::Texture); + SubmitTransformUniform(translation); + if (texture != TextureEnableWithoutBinding) + glBindTexture(GL_TEXTURE_2D, (GLuint)texture); + } + else + { + UseProgram(ProgramId::Color); + glBindTexture(GL_TEXTURE_2D, 0); + SubmitTransformUniform(translation); + } + + glBindVertexArray(geometry->vao); + glDrawElements(GL_TRIANGLES, geometry->draw_count, GL_UNSIGNED_INT, (const GLvoid*)0); + + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); + + Gfx::CheckGLError("RenderCompiledGeometry"); +} + +void RenderInterface_GL3::ReleaseGeometry(Rml::CompiledGeometryHandle handle) +{ + Gfx::CompiledGeometryData* geometry = (Gfx::CompiledGeometryData*)handle; + + glDeleteVertexArrays(1, &geometry->vao); + glDeleteBuffers(1, &geometry->vbo); + glDeleteBuffers(1, &geometry->ibo); + + delete geometry; +} + +/// Flip the vertical axis of the rectangle, and move its origin to the vertically opposite side of the viewport. +/// @note Changes the coordinate system from RmlUi to OpenGL, or equivalently in reverse. +/// @note The Rectangle::Top and Rectangle::Bottom members will have reverse meaning in the returned rectangle. +static Rml::Rectanglei VerticallyFlipped(Rml::Rectanglei rect, int viewport_height) +{ + RMLUI_ASSERT(rect.Valid()); + Rml::Rectanglei flipped_rect = rect; + flipped_rect.p0.y = viewport_height - rect.p1.y; + flipped_rect.p1.y = viewport_height - rect.p0.y; + return flipped_rect; +} + +void RenderInterface_GL3::SetScissor(Rml::Rectanglei region, bool vertically_flip) +{ + if (region.Valid() != scissor_state.Valid()) + { + if (region.Valid()) + glEnable(GL_SCISSOR_TEST); + else + glDisable(GL_SCISSOR_TEST); + } + + if (region.Valid() && vertically_flip) + region = VerticallyFlipped(region, viewport_height); + + if (region.Valid() && region != scissor_state) + { + // Some render APIs don't like offscreen positions (WebGL in particular), so clamp them to the viewport. + const int x = Rml::Math::Clamp(region.Left(), 0, viewport_width); + const int y = Rml::Math::Clamp(viewport_height - region.Bottom(), 0, viewport_height); + + glScissor(x, y, region.Width(), region.Height()); + } + + Gfx::CheckGLError("SetScissorRegion"); + scissor_state = region; +} + +void RenderInterface_GL3::EnableScissorRegion(bool enable) +{ + // Assume enable is immediately followed by a SetScissorRegion() call, and ignore it here. + if (!enable) + SetScissor(Rml::Rectanglei::MakeInvalid(), false); +} + +void RenderInterface_GL3::SetScissorRegion(Rml::Rectanglei region) +{ + SetScissor(region); +} + +void RenderInterface_GL3::EnableClipMask(bool enable) +{ + if (enable) + glEnable(GL_STENCIL_TEST); + else + glDisable(GL_STENCIL_TEST); +} + +void RenderInterface_GL3::RenderToClipMask(Rml::ClipMaskOperation operation, Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation) +{ + RMLUI_ASSERT(glIsEnabled(GL_STENCIL_TEST)); + using Rml::ClipMaskOperation; + + GLint stencil_write_value = 1; + GLint stencil_test_value = 1; + switch (operation) + { + case ClipMaskOperation::Set: + { + // @performance Increment the reference value instead of clearing each time. + glClear(GL_STENCIL_BUFFER_BIT); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + } + break; + case ClipMaskOperation::SetInverse: + { + glClearStencil(1); + glClear(GL_STENCIL_BUFFER_BIT); + glClearStencil(0); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + stencil_write_value = 0; + } + break; + case ClipMaskOperation::Intersect: + { + glStencilOp(GL_KEEP, GL_KEEP, GL_INCR); + glGetIntegerv(GL_STENCIL_REF, &stencil_test_value); + stencil_test_value += 1; + } + break; + } + + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + glStencilFunc(GL_ALWAYS, stencil_write_value, GLuint(-1)); + + RenderGeometry(geometry, translation, {}); + + // Restore state + // @performance Cache state so we don't toggle it unnecessarily. + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + glStencilFunc(GL_EQUAL, stencil_test_value, GLuint(-1)); +} + +// Set to byte packing, or the compiler will expand our struct, which means it won't read correctly from file +#pragma pack(1) +struct TGAHeader { + char idLength; + char colourMapType; + char dataType; + short int colourMapOrigin; + short int colourMapLength; + char colourMapDepth; + short int xOrigin; + short int yOrigin; + short int width; + short int height; + char bitsPerPixel; + char imageDescriptor; +}; +// Restore packing +#pragma pack() + +Rml::TextureHandle RenderInterface_GL3::LoadTexture(Rml::Vector2i& texture_dimensions, const Rml::String& source) +{ + Rml::FileInterface* file_interface = Rml::GetFileInterface(); + Rml::FileHandle file_handle = file_interface->Open(source); + if (!file_handle) + { + return false; + } + + file_interface->Seek(file_handle, 0, SEEK_END); + size_t buffer_size = file_interface->Tell(file_handle); + file_interface->Seek(file_handle, 0, SEEK_SET); + + if (buffer_size <= sizeof(TGAHeader)) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "Texture file size is smaller than TGAHeader, file is not a valid TGA image."); + file_interface->Close(file_handle); + return false; + } + + using Rml::byte; + Rml::UniquePtr<byte[]> buffer(new byte[buffer_size]); + file_interface->Read(buffer.get(), buffer_size, file_handle); + file_interface->Close(file_handle); + + TGAHeader header; + memcpy(&header, buffer.get(), sizeof(TGAHeader)); + + int color_mode = header.bitsPerPixel / 8; + const size_t image_size = header.width * header.height * 4; // We always make 32bit textures + + if (header.dataType != 2) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "Only 24/32bit uncompressed TGAs are supported."); + return false; + } + + // Ensure we have at least 3 colors + if (color_mode < 3) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "Only 24 and 32bit textures are supported."); + return false; + } + + const byte* image_src = buffer.get() + sizeof(TGAHeader); + Rml::UniquePtr<byte[]> image_dest_buffer(new byte[image_size]); + byte* image_dest = image_dest_buffer.get(); + const bool top_to_bottom_order = ((header.imageDescriptor & 32) != 0); + + // Targa is BGR, swap to RGB, flip Y axis as necessary, and convert to premultiplied alpha. + for (long y = 0; y < header.height; y++) + { + long read_index = y * header.width * color_mode; + long write_index = top_to_bottom_order ? (y * header.width * 4) : (header.height - y - 1) * header.width * 4; + for (long x = 0; x < header.width; x++) + { + image_dest[write_index] = image_src[read_index + 2]; + image_dest[write_index + 1] = image_src[read_index + 1]; + image_dest[write_index + 2] = image_src[read_index]; + if (color_mode == 4) + { + const byte alpha = image_src[read_index + 3]; + for (size_t j = 0; j < 3; j++) + image_dest[write_index + j] = byte((image_dest[write_index + j] * alpha) / 255); + image_dest[write_index + 3] = alpha; + } + else + image_dest[write_index + 3] = 255; + + write_index += 4; + read_index += color_mode; + } + } + + texture_dimensions.x = header.width; + texture_dimensions.y = header.height; + + return GenerateTexture({image_dest, image_size}, texture_dimensions); +} + +Rml::TextureHandle RenderInterface_GL3::GenerateTexture(Rml::Span<const Rml::byte> source_data, Rml::Vector2i source_dimensions) +{ + RMLUI_ASSERT(source_data.data() && source_data.size() == size_t(source_dimensions.x * source_dimensions.y * 4)); + + GLuint texture_id = Gfx::CreateTexture(source_data, source_dimensions); + if (texture_id == 0) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to generate texture."); + return {}; + } + return (Rml::TextureHandle)texture_id; +} + +void RenderInterface_GL3::DrawFullscreenQuad() +{ + RenderGeometry(fullscreen_quad_geometry, {}, RenderInterface_GL3::TexturePostprocess); +} + +void RenderInterface_GL3::DrawFullscreenQuad(Rml::Vector2f uv_offset, Rml::Vector2f uv_scaling) +{ + Rml::Mesh mesh; + Rml::MeshUtilities::GenerateQuad(mesh, Rml::Vector2f(-1), Rml::Vector2f(2), {}); + if (uv_offset != Rml::Vector2f() || uv_scaling != Rml::Vector2f(1.f)) + { + for (Rml::Vertex& vertex : mesh.vertices) + vertex.tex_coord = (vertex.tex_coord * uv_scaling) + uv_offset; + } + const Rml::CompiledGeometryHandle geometry = CompileGeometry(mesh.vertices, mesh.indices); + RenderGeometry(geometry, {}, RenderInterface_GL3::TexturePostprocess); + ReleaseGeometry(geometry); +} + +static Rml::Colourf ConvertToColorf(Rml::ColourbPremultiplied c0) +{ + Rml::Colourf result; + for (int i = 0; i < 4; i++) + result[i] = (1.f / 255.f) * float(c0[i]); + return result; +} + +static void SigmaToParameters(const float desired_sigma, int& out_pass_level, float& out_sigma) +{ + constexpr int max_num_passes = 10; + static_assert(max_num_passes < 31, ""); + constexpr float max_single_pass_sigma = 3.0f; + out_pass_level = Rml::Math::Clamp(Rml::Math::Log2(int(desired_sigma * (2.f / max_single_pass_sigma))), 0, max_num_passes); + out_sigma = Rml::Math::Clamp(desired_sigma / float(1 << out_pass_level), 0.0f, max_single_pass_sigma); +} + +static void SetTexCoordLimits(GLint tex_coord_min_location, GLint tex_coord_max_location, Rml::Rectanglei rectangle_flipped, + Rml::Vector2i framebuffer_size) +{ + // Offset by half-texel values so that texture lookups are clamped to fragment centers, thereby avoiding color + // bleeding from neighboring texels due to bilinear interpolation. + const Rml::Vector2f min = (Rml::Vector2f(rectangle_flipped.p0) + Rml::Vector2f(0.5f)) / Rml::Vector2f(framebuffer_size); + const Rml::Vector2f max = (Rml::Vector2f(rectangle_flipped.p1) - Rml::Vector2f(0.5f)) / Rml::Vector2f(framebuffer_size); + + glUniform2f(tex_coord_min_location, min.x, min.y); + glUniform2f(tex_coord_max_location, max.x, max.y); +} + +static void SetBlurWeights(GLint weights_location, float sigma) +{ + constexpr int num_weights = BLUR_NUM_WEIGHTS; + float weights[num_weights]; + float normalization = 0.0f; + for (int i = 0; i < num_weights; i++) + { + if (Rml::Math::Absolute(sigma) < 0.1f) + weights[i] = float(i == 0); + else + weights[i] = Rml::Math::Exp(-float(i * i) / (2.0f * sigma * sigma)) / (Rml::Math::SquareRoot(2.f * Rml::Math::RMLUI_PI) * sigma); + + normalization += (i == 0 ? 1.f : 2.0f) * weights[i]; + } + for (int i = 0; i < num_weights; i++) + weights[i] /= normalization; + + glUniform1fv(weights_location, (GLsizei)num_weights, &weights[0]); +} + +void RenderInterface_GL3::RenderBlur(float sigma, const Gfx::FramebufferData& source_destination, const Gfx::FramebufferData& temp, + const Rml::Rectanglei window_flipped) +{ + RMLUI_ASSERT(&source_destination != &temp && source_destination.width == temp.width && source_destination.height == temp.height); + RMLUI_ASSERT(window_flipped.Valid()); + + int pass_level = 0; + SigmaToParameters(sigma, pass_level, sigma); + + const Rml::Rectanglei original_scissor = scissor_state; + + // Begin by downscaling so that the blur pass can be done at a reduced resolution for large sigma. + Rml::Rectanglei scissor = window_flipped; + + UseProgram(ProgramId::Passthrough); + SetScissor(scissor, true); + + // Downscale by iterative half-scaling with bilinear filtering, to reduce aliasing. + glViewport(0, 0, source_destination.width / 2, source_destination.height / 2); + + // Scale UVs if we have even dimensions, such that texture fetches align perfectly between texels, thereby producing a 50% blend of + // neighboring texels. + const Rml::Vector2f uv_scaling = {(source_destination.width % 2 == 1) ? (1.f - 1.f / float(source_destination.width)) : 1.f, + (source_destination.height % 2 == 1) ? (1.f - 1.f / float(source_destination.height)) : 1.f}; + + for (int i = 0; i < pass_level; i++) + { + scissor.p0 = (scissor.p0 + Rml::Vector2i(1)) / 2; + scissor.p1 = Rml::Math::Max(scissor.p1 / 2, scissor.p0); + const bool from_source = (i % 2 == 0); + Gfx::BindTexture(from_source ? source_destination : temp); + glBindFramebuffer(GL_FRAMEBUFFER, (from_source ? temp : source_destination).framebuffer); + SetScissor(scissor, true); + + DrawFullscreenQuad({}, uv_scaling); + } + + glViewport(0, 0, source_destination.width, source_destination.height); + + // Ensure texture data end up in the temp buffer. Depending on the last downscaling, we might need to move it from the source_destination buffer. + const bool transfer_to_temp_buffer = (pass_level % 2 == 0); + if (transfer_to_temp_buffer) + { + Gfx::BindTexture(source_destination); + glBindFramebuffer(GL_FRAMEBUFFER, temp.framebuffer); + DrawFullscreenQuad(); + } + + // Set up uniforms. + UseProgram(ProgramId::Blur); + SetBlurWeights(GetUniformLocation(UniformId::Weights), sigma); + SetTexCoordLimits(GetUniformLocation(UniformId::TexCoordMin), GetUniformLocation(UniformId::TexCoordMax), scissor, + {source_destination.width, source_destination.height}); + + const GLint texel_offset_location = GetUniformLocation(UniformId::TexelOffset); + auto SetTexelOffset = [texel_offset_location](Rml::Vector2f blur_direction, int texture_dimension) { + const Rml::Vector2f texel_offset = blur_direction * (1.0f / float(texture_dimension)); + glUniform2f(texel_offset_location, texel_offset.x, texel_offset.y); + }; + + // Blur render pass - vertical. + Gfx::BindTexture(temp); + glBindFramebuffer(GL_FRAMEBUFFER, source_destination.framebuffer); + + SetTexelOffset({0.f, 1.f}, temp.height); + DrawFullscreenQuad(); + + // Blur render pass - horizontal. + Gfx::BindTexture(source_destination); + glBindFramebuffer(GL_FRAMEBUFFER, temp.framebuffer); + + // Add a 1px transparent border around the blur region by first clearing with a padded scissor. This helps prevent + // artifacts when upscaling the blur result in the later step. On Intel and AMD, we have observed that during + // blitting with linear filtering, pixels outside the 'src' region can be blended into the output. On the other + // hand, it looks like Nvidia clamps the pixels to the source edge, which is what we really want. Regardless, we + // work around the issue with this extra step. + SetScissor(scissor.Extend(1), true); + glClear(GL_COLOR_BUFFER_BIT); + SetScissor(scissor, true); + + SetTexelOffset({1.f, 0.f}, source_destination.width); + DrawFullscreenQuad(); + + // Blit the blurred image to the scissor region with upscaling. + SetScissor(window_flipped, true); + glBindFramebuffer(GL_READ_FRAMEBUFFER, temp.framebuffer); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, source_destination.framebuffer); + + const Rml::Vector2i src_min = scissor.p0; + const Rml::Vector2i src_max = scissor.p1; + const Rml::Vector2i dst_min = window_flipped.p0; + const Rml::Vector2i dst_max = window_flipped.p1; + glBlitFramebuffer(src_min.x, src_min.y, src_max.x, src_max.y, dst_min.x, dst_min.y, dst_max.x, dst_max.y, GL_COLOR_BUFFER_BIT, GL_LINEAR); + + // The above upscale blit might be jittery at low resolutions (large pass levels). This is especially noticeable when moving an element with + // backdrop blur around or when trying to click/hover an element within a blurred region since it may be rendered at an offset. For more stable + // and accurate rendering we next upscale the blur image by an exact power-of-two. However, this may not fill the edges completely so we need to + // do the above first. Note that this strategy may sometimes result in visible seams. Alternatively, we could try to enlarge the window to the + // next power-of-two size and then downsample and blur that. + const Rml::Vector2i target_min = src_min * (1 << pass_level); + const Rml::Vector2i target_max = src_max * (1 << pass_level); + if (target_min != dst_min || target_max != dst_max) + { + glBlitFramebuffer(src_min.x, src_min.y, src_max.x, src_max.y, target_min.x, target_min.y, target_max.x, target_max.y, GL_COLOR_BUFFER_BIT, + GL_LINEAR); + } + + // Restore render state. + SetScissor(original_scissor); + + Gfx::CheckGLError("Blur"); +} + +void RenderInterface_GL3::ReleaseTexture(Rml::TextureHandle texture_handle) +{ + glDeleteTextures(1, (GLuint*)&texture_handle); +} + +void RenderInterface_GL3::SetTransform(const Rml::Matrix4f* new_transform) +{ + transform = (new_transform ? (projection * (*new_transform)) : projection); + program_transform_dirty.set(); +} + +enum class FilterType { Invalid = 0, Passthrough, Blur, DropShadow, ColorMatrix, MaskImage }; +struct CompiledFilter { + FilterType type; + + // Passthrough + float blend_factor; + + // Blur + float sigma; + + // Drop shadow + Rml::Vector2f offset; + Rml::ColourbPremultiplied color; + + // ColorMatrix + Rml::Matrix4f color_matrix; +}; + +Rml::CompiledFilterHandle RenderInterface_GL3::CompileFilter(const Rml::String& name, const Rml::Dictionary& parameters) +{ + CompiledFilter filter = {}; + + if (name == "opacity") + { + filter.type = FilterType::Passthrough; + filter.blend_factor = Rml::Get(parameters, "value", 1.0f); + } + else if (name == "blur") + { + filter.type = FilterType::Blur; + filter.sigma = Rml::Get(parameters, "sigma", 1.0f); + } + else if (name == "drop-shadow") + { + filter.type = FilterType::DropShadow; + filter.sigma = Rml::Get(parameters, "sigma", 0.f); + filter.color = Rml::Get(parameters, "color", Rml::Colourb()).ToPremultiplied(); + filter.offset = Rml::Get(parameters, "offset", Rml::Vector2f(0.f)); + } + else if (name == "brightness") + { + filter.type = FilterType::ColorMatrix; + const float value = Rml::Get(parameters, "value", 1.0f); + filter.color_matrix = Rml::Matrix4f::Diag(value, value, value, 1.f); + } + else if (name == "contrast") + { + filter.type = FilterType::ColorMatrix; + const float value = Rml::Get(parameters, "value", 1.0f); + const float grayness = 0.5f - 0.5f * value; + filter.color_matrix = Rml::Matrix4f::Diag(value, value, value, 1.f); + filter.color_matrix.SetColumn(3, Rml::Vector4f(grayness, grayness, grayness, 1.f)); + } + else if (name == "invert") + { + filter.type = FilterType::ColorMatrix; + const float value = Rml::Math::Clamp(Rml::Get(parameters, "value", 1.0f), 0.f, 1.f); + const float inverted = 1.f - 2.f * value; + filter.color_matrix = Rml::Matrix4f::Diag(inverted, inverted, inverted, 1.f); + filter.color_matrix.SetColumn(3, Rml::Vector4f(value, value, value, 1.f)); + } + else if (name == "grayscale") + { + filter.type = FilterType::ColorMatrix; + const float value = Rml::Get(parameters, "value", 1.0f); + const float rev_value = 1.f - value; + const Rml::Vector3f gray = value * Rml::Vector3f(0.2126f, 0.7152f, 0.0722f); + // clang-format off + filter.color_matrix = Rml::Matrix4f::FromRows( + {gray.x + rev_value, gray.y, gray.z, 0.f}, + {gray.x, gray.y + rev_value, gray.z, 0.f}, + {gray.x, gray.y, gray.z + rev_value, 0.f}, + {0.f, 0.f, 0.f, 1.f} + ); + // clang-format on + } + else if (name == "sepia") + { + filter.type = FilterType::ColorMatrix; + const float value = Rml::Get(parameters, "value", 1.0f); + const float rev_value = 1.f - value; + const Rml::Vector3f r_mix = value * Rml::Vector3f(0.393f, 0.769f, 0.189f); + const Rml::Vector3f g_mix = value * Rml::Vector3f(0.349f, 0.686f, 0.168f); + const Rml::Vector3f b_mix = value * Rml::Vector3f(0.272f, 0.534f, 0.131f); + // clang-format off + filter.color_matrix = Rml::Matrix4f::FromRows( + {r_mix.x + rev_value, r_mix.y, r_mix.z, 0.f}, + {g_mix.x, g_mix.y + rev_value, g_mix.z, 0.f}, + {b_mix.x, b_mix.y, b_mix.z + rev_value, 0.f}, + {0.f, 0.f, 0.f, 1.f} + ); + // clang-format on + } + else if (name == "hue-rotate") + { + // Hue-rotation and saturation values based on: https://www.w3.org/TR/filter-effects-1/#attr-valuedef-type-huerotate + filter.type = FilterType::ColorMatrix; + const float value = Rml::Get(parameters, "value", 1.0f); + const float s = Rml::Math::Sin(value); + const float c = Rml::Math::Cos(value); + // clang-format off + filter.color_matrix = Rml::Matrix4f::FromRows( + {0.213f + 0.787f * c - 0.213f * s, 0.715f - 0.715f * c - 0.715f * s, 0.072f - 0.072f * c + 0.928f * s, 0.f}, + {0.213f - 0.213f * c + 0.143f * s, 0.715f + 0.285f * c + 0.140f * s, 0.072f - 0.072f * c - 0.283f * s, 0.f}, + {0.213f - 0.213f * c - 0.787f * s, 0.715f - 0.715f * c + 0.715f * s, 0.072f + 0.928f * c + 0.072f * s, 0.f}, + {0.f, 0.f, 0.f, 1.f} + ); + // clang-format on + } + else if (name == "saturate") + { + filter.type = FilterType::ColorMatrix; + const float value = Rml::Get(parameters, "value", 1.0f); + // clang-format off + filter.color_matrix = Rml::Matrix4f::FromRows( + {0.213f + 0.787f * value, 0.715f - 0.715f * value, 0.072f - 0.072f * value, 0.f}, + {0.213f - 0.213f * value, 0.715f + 0.285f * value, 0.072f - 0.072f * value, 0.f}, + {0.213f - 0.213f * value, 0.715f - 0.715f * value, 0.072f + 0.928f * value, 0.f}, + {0.f, 0.f, 0.f, 1.f} + ); + // clang-format on + } + + if (filter.type != FilterType::Invalid) + return reinterpret_cast<Rml::CompiledFilterHandle>(new CompiledFilter(std::move(filter))); + + Rml::Log::Message(Rml::Log::LT_WARNING, "Unsupported filter type '%s'.", name.c_str()); + return {}; +} + +void RenderInterface_GL3::ReleaseFilter(Rml::CompiledFilterHandle filter) +{ + delete reinterpret_cast<CompiledFilter*>(filter); +} + +enum class CompiledShaderType { Invalid = 0, Gradient, Creation }; +struct CompiledShader { + CompiledShaderType type; + + // Gradient + ShaderGradientFunction gradient_function; + Rml::Vector2f p; + Rml::Vector2f v; + Rml::Vector<float> stop_positions; + Rml::Vector<Rml::Colourf> stop_colors; + + // Shader + Rml::Vector2f dimensions; +}; + +Rml::CompiledShaderHandle RenderInterface_GL3::CompileShader(const Rml::String& name, const Rml::Dictionary& parameters) +{ + auto ApplyColorStopList = [](CompiledShader& shader, const Rml::Dictionary& shader_parameters) { + auto it = shader_parameters.find("color_stop_list"); + RMLUI_ASSERT(it != shader_parameters.end() && it->second.GetType() == Rml::Variant::COLORSTOPLIST); + const Rml::ColorStopList& color_stop_list = it->second.GetReference<Rml::ColorStopList>(); + const int num_stops = Rml::Math::Min((int)color_stop_list.size(), MAX_NUM_STOPS); + + shader.stop_positions.resize(num_stops); + shader.stop_colors.resize(num_stops); + for (int i = 0; i < num_stops; i++) + { + const Rml::ColorStop& stop = color_stop_list[i]; + RMLUI_ASSERT(stop.position.unit == Rml::Unit::NUMBER); + shader.stop_positions[i] = stop.position.number; + shader.stop_colors[i] = ConvertToColorf(stop.color); + } + }; + + CompiledShader shader = {}; + + if (name == "linear-gradient") + { + shader.type = CompiledShaderType::Gradient; + const bool repeating = Rml::Get(parameters, "repeating", false); + shader.gradient_function = (repeating ? ShaderGradientFunction::RepeatingLinear : ShaderGradientFunction::Linear); + shader.p = Rml::Get(parameters, "p0", Rml::Vector2f(0.f)); + shader.v = Rml::Get(parameters, "p1", Rml::Vector2f(0.f)) - shader.p; + ApplyColorStopList(shader, parameters); + } + else if (name == "radial-gradient") + { + shader.type = CompiledShaderType::Gradient; + const bool repeating = Rml::Get(parameters, "repeating", false); + shader.gradient_function = (repeating ? ShaderGradientFunction::RepeatingRadial : ShaderGradientFunction::Radial); + shader.p = Rml::Get(parameters, "center", Rml::Vector2f(0.f)); + shader.v = Rml::Vector2f(1.f) / Rml::Get(parameters, "radius", Rml::Vector2f(1.f)); + ApplyColorStopList(shader, parameters); + } + else if (name == "conic-gradient") + { + shader.type = CompiledShaderType::Gradient; + const bool repeating = Rml::Get(parameters, "repeating", false); + shader.gradient_function = (repeating ? ShaderGradientFunction::RepeatingConic : ShaderGradientFunction::Conic); + shader.p = Rml::Get(parameters, "center", Rml::Vector2f(0.f)); + const float angle = Rml::Get(parameters, "angle", 0.f); + shader.v = {Rml::Math::Cos(angle), Rml::Math::Sin(angle)}; + ApplyColorStopList(shader, parameters); + } + else if (name == "shader") + { + const Rml::String value = Rml::Get(parameters, "value", Rml::String()); + if (value == "creation") + { + shader.type = CompiledShaderType::Creation; + shader.dimensions = Rml::Get(parameters, "dimensions", Rml::Vector2f(0.f)); + } + } + + if (shader.type != CompiledShaderType::Invalid) + return reinterpret_cast<Rml::CompiledShaderHandle>(new CompiledShader(std::move(shader))); + + Rml::Log::Message(Rml::Log::LT_WARNING, "Unsupported shader type '%s'.", name.c_str()); + return {}; +} + +void RenderInterface_GL3::RenderShader(Rml::CompiledShaderHandle shader_handle, Rml::CompiledGeometryHandle geometry_handle, + Rml::Vector2f translation, Rml::TextureHandle /*texture*/) +{ + RMLUI_ASSERT(shader_handle && geometry_handle); + const CompiledShader& shader = *reinterpret_cast<CompiledShader*>(shader_handle); + const CompiledShaderType type = shader.type; + const Gfx::CompiledGeometryData& geometry = *reinterpret_cast<Gfx::CompiledGeometryData*>(geometry_handle); + + switch (type) + { + case CompiledShaderType::Gradient: + { + RMLUI_ASSERT(shader.stop_positions.size() == shader.stop_colors.size()); + const int num_stops = (int)shader.stop_positions.size(); + + UseProgram(ProgramId::Gradient); + glUniform1i(GetUniformLocation(UniformId::Func), static_cast<int>(shader.gradient_function)); + glUniform2f(GetUniformLocation(UniformId::P), shader.p.x, shader.p.y); + glUniform2f(GetUniformLocation(UniformId::V), shader.v.x, shader.v.y); + glUniform1i(GetUniformLocation(UniformId::NumStops), num_stops); + glUniform1fv(GetUniformLocation(UniformId::StopPositions), num_stops, shader.stop_positions.data()); + glUniform4fv(GetUniformLocation(UniformId::StopColors), num_stops, shader.stop_colors[0]); + + SubmitTransformUniform(translation); + glBindVertexArray(geometry.vao); + glDrawElements(GL_TRIANGLES, geometry.draw_count, GL_UNSIGNED_INT, (const GLvoid*)0); + glBindVertexArray(0); + } + break; + case CompiledShaderType::Creation: + { + const double time = Rml::GetSystemInterface()->GetElapsedTime(); + + UseProgram(ProgramId::Creation); + glUniform1f(GetUniformLocation(UniformId::Value), (float)time); + glUniform2f(GetUniformLocation(UniformId::Dimensions), shader.dimensions.x, shader.dimensions.y); + + SubmitTransformUniform(translation); + glBindVertexArray(geometry.vao); + glDrawElements(GL_TRIANGLES, geometry.draw_count, GL_UNSIGNED_INT, (const GLvoid*)0); + glBindVertexArray(0); + } + break; + case CompiledShaderType::Invalid: + { + Rml::Log::Message(Rml::Log::LT_WARNING, "Unhandled render shader %d.", (int)type); + } + break; + } + + Gfx::CheckGLError("RenderShader"); +} + +void RenderInterface_GL3::ReleaseShader(Rml::CompiledShaderHandle shader_handle) +{ + delete reinterpret_cast<CompiledShader*>(shader_handle); +} + +void RenderInterface_GL3::BlitLayerToPostprocessPrimary(Rml::LayerHandle layer_handle) +{ + const Gfx::FramebufferData& source = render_layers.GetLayer(layer_handle); + const Gfx::FramebufferData& destination = render_layers.GetPostprocessPrimary(); + glBindFramebuffer(GL_READ_FRAMEBUFFER, source.framebuffer); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destination.framebuffer); + + // Blit and resolve MSAA. Any active scissor state will restrict the size of the blit region. + glBlitFramebuffer(0, 0, source.width, source.height, 0, 0, destination.width, destination.height, GL_COLOR_BUFFER_BIT, GL_NEAREST); +} + +void RenderInterface_GL3::RenderFilters(Rml::Span<const Rml::CompiledFilterHandle> filter_handles) +{ + for (const Rml::CompiledFilterHandle filter_handle : filter_handles) + { + const CompiledFilter& filter = *reinterpret_cast<const CompiledFilter*>(filter_handle); + const FilterType type = filter.type; + + switch (type) + { + case FilterType::Passthrough: + { + UseProgram(ProgramId::Passthrough); + glBlendFunc(GL_CONSTANT_COLOR, GL_ZERO); + glBlendColor(filter.blend_factor, filter.blend_factor, filter.blend_factor, filter.blend_factor); + + const Gfx::FramebufferData& source = render_layers.GetPostprocessPrimary(); + const Gfx::FramebufferData& destination = render_layers.GetPostprocessSecondary(); + Gfx::BindTexture(source); + glBindFramebuffer(GL_FRAMEBUFFER, destination.framebuffer); + + DrawFullscreenQuad(); + + render_layers.SwapPostprocessPrimarySecondary(); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + } + break; + case FilterType::Blur: + { + glDisable(GL_BLEND); + + const Gfx::FramebufferData& source_destination = render_layers.GetPostprocessPrimary(); + const Gfx::FramebufferData& temp = render_layers.GetPostprocessSecondary(); + + const Rml::Rectanglei window_flipped = VerticallyFlipped(scissor_state, viewport_height); + RenderBlur(filter.sigma, source_destination, temp, window_flipped); + + glEnable(GL_BLEND); + } + break; + case FilterType::DropShadow: + { + UseProgram(ProgramId::DropShadow); + glDisable(GL_BLEND); + + Rml::Colourf color = ConvertToColorf(filter.color); + glUniform4fv(GetUniformLocation(UniformId::Color), 1, &color[0]); + + const Gfx::FramebufferData& primary = render_layers.GetPostprocessPrimary(); + const Gfx::FramebufferData& secondary = render_layers.GetPostprocessSecondary(); + Gfx::BindTexture(primary); + glBindFramebuffer(GL_FRAMEBUFFER, secondary.framebuffer); + + const Rml::Rectanglei window_flipped = VerticallyFlipped(scissor_state, viewport_height); + SetTexCoordLimits(GetUniformLocation(UniformId::TexCoordMin), GetUniformLocation(UniformId::TexCoordMax), window_flipped, + {primary.width, primary.height}); + + const Rml::Vector2f uv_offset = filter.offset / Rml::Vector2f(-(float)viewport_width, (float)viewport_height); + DrawFullscreenQuad(uv_offset); + + if (filter.sigma >= 0.5f) + { + const Gfx::FramebufferData& tertiary = render_layers.GetPostprocessTertiary(); + RenderBlur(filter.sigma, secondary, tertiary, window_flipped); + } + + UseProgram(ProgramId::Passthrough); + BindTexture(primary); + glEnable(GL_BLEND); + DrawFullscreenQuad(); + + render_layers.SwapPostprocessPrimarySecondary(); + } + break; + case FilterType::ColorMatrix: + { + UseProgram(ProgramId::ColorMatrix); + glDisable(GL_BLEND); + + const GLint uniform_location = program_data->uniforms.Get(ProgramId::ColorMatrix, UniformId::ColorMatrix); + constexpr bool transpose = std::is_same<decltype(filter.color_matrix), Rml::RowMajorMatrix4f>::value; + glUniformMatrix4fv(uniform_location, 1, transpose, filter.color_matrix.data()); + + const Gfx::FramebufferData& source = render_layers.GetPostprocessPrimary(); + const Gfx::FramebufferData& destination = render_layers.GetPostprocessSecondary(); + Gfx::BindTexture(source); + glBindFramebuffer(GL_FRAMEBUFFER, destination.framebuffer); + + DrawFullscreenQuad(); + + render_layers.SwapPostprocessPrimarySecondary(); + glEnable(GL_BLEND); + } + break; + case FilterType::MaskImage: + { + UseProgram(ProgramId::BlendMask); + glDisable(GL_BLEND); + + const Gfx::FramebufferData& source = render_layers.GetPostprocessPrimary(); + const Gfx::FramebufferData& blend_mask = render_layers.GetBlendMask(); + const Gfx::FramebufferData& destination = render_layers.GetPostprocessSecondary(); + + Gfx::BindTexture(source); + glActiveTexture(GL_TEXTURE1); + Gfx::BindTexture(blend_mask); + glActiveTexture(GL_TEXTURE0); + + glBindFramebuffer(GL_FRAMEBUFFER, destination.framebuffer); + + DrawFullscreenQuad(); + + render_layers.SwapPostprocessPrimarySecondary(); + glEnable(GL_BLEND); + } + break; + case FilterType::Invalid: + { + Rml::Log::Message(Rml::Log::LT_WARNING, "Unhandled render filter %d.", (int)type); + } + break; + } + } + + Gfx::CheckGLError("RenderFilter"); +} + +Rml::LayerHandle RenderInterface_GL3::PushLayer() +{ + const Rml::LayerHandle layer_handle = render_layers.PushLayer(); + + glBindFramebuffer(GL_FRAMEBUFFER, render_layers.GetLayer(layer_handle).framebuffer); + glClear(GL_COLOR_BUFFER_BIT); + + return layer_handle; +} + +void RenderInterface_GL3::CompositeLayers(Rml::LayerHandle source_handle, Rml::LayerHandle destination_handle, Rml::BlendMode blend_mode, + Rml::Span<const Rml::CompiledFilterHandle> filters) +{ + using Rml::BlendMode; + + // Blit source layer to postprocessing buffer. Do this regardless of whether we actually have any filters to be + // applied, because we need to resolve the multi-sampled framebuffer in any case. + // @performance If we have BlendMode::Replace and no filters or mask then we can just blit directly to the destination. + BlitLayerToPostprocessPrimary(source_handle); + + // Render the filters, the PostprocessPrimary framebuffer is used for both input and output. + RenderFilters(filters); + + // Render to the destination layer. + glBindFramebuffer(GL_FRAMEBUFFER, render_layers.GetLayer(destination_handle).framebuffer); + Gfx::BindTexture(render_layers.GetPostprocessPrimary()); + + UseProgram(ProgramId::Passthrough); + + if (blend_mode == BlendMode::Replace) + glDisable(GL_BLEND); + + DrawFullscreenQuad(); + + if (blend_mode == BlendMode::Replace) + glEnable(GL_BLEND); + + if (destination_handle != render_layers.GetTopLayerHandle()) + glBindFramebuffer(GL_FRAMEBUFFER, render_layers.GetTopLayer().framebuffer); + + Gfx::CheckGLError("CompositeLayers"); +} + +void RenderInterface_GL3::PopLayer() +{ + render_layers.PopLayer(); + glBindFramebuffer(GL_FRAMEBUFFER, render_layers.GetTopLayer().framebuffer); +} + +Rml::TextureHandle RenderInterface_GL3::SaveLayerAsTexture() +{ + RMLUI_ASSERT(scissor_state.Valid()); + const Rml::Rectanglei bounds = scissor_state; + + GLuint render_texture = Gfx::CreateTexture({}, bounds.Size()); + if (render_texture == 0) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to create render texture."); + return {}; + } + + BlitLayerToPostprocessPrimary(render_layers.GetTopLayerHandle()); + + EnableScissorRegion(false); + + const Gfx::FramebufferData& source = render_layers.GetPostprocessPrimary(); + const Gfx::FramebufferData& destination = render_layers.GetPostprocessSecondary(); + glBindFramebuffer(GL_READ_FRAMEBUFFER, source.framebuffer); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destination.framebuffer); + + // Flip the image vertically, as that convention is used for textures, and move to origin. + glBlitFramebuffer( // + bounds.Left(), source.height - bounds.Bottom(), // src0 + bounds.Right(), source.height - bounds.Top(), // src1 + 0, bounds.Height(), // dst0 + bounds.Width(), 0, // dst1 + GL_COLOR_BUFFER_BIT, GL_NEAREST // + ); + + glBindTexture(GL_TEXTURE_2D, render_texture); + + const Gfx::FramebufferData& texture_source = destination; + glBindFramebuffer(GL_READ_FRAMEBUFFER, texture_source.framebuffer); + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, bounds.Width(), bounds.Height()); + + SetScissor(bounds); + glBindFramebuffer(GL_FRAMEBUFFER, render_layers.GetTopLayer().framebuffer); + Gfx::CheckGLError("SaveLayerAsTexture"); + + return (Rml::TextureHandle)render_texture; +} + +Rml::CompiledFilterHandle RenderInterface_GL3::SaveLayerAsMaskImage() +{ + BlitLayerToPostprocessPrimary(render_layers.GetTopLayerHandle()); + + const Gfx::FramebufferData& source = render_layers.GetPostprocessPrimary(); + const Gfx::FramebufferData& destination = render_layers.GetBlendMask(); + + glBindFramebuffer(GL_FRAMEBUFFER, destination.framebuffer); + BindTexture(source); + UseProgram(ProgramId::Passthrough); + glDisable(GL_BLEND); + + DrawFullscreenQuad(); + + glEnable(GL_BLEND); + glBindFramebuffer(GL_FRAMEBUFFER, render_layers.GetTopLayer().framebuffer); + Gfx::CheckGLError("SaveLayerAsMaskImage"); + + CompiledFilter filter = {}; + filter.type = FilterType::MaskImage; + return reinterpret_cast<Rml::CompiledFilterHandle>(new CompiledFilter(std::move(filter))); +} + +void RenderInterface_GL3::UseProgram(ProgramId program_id) +{ + RMLUI_ASSERT(program_data); + if (active_program != program_id) + { + if (program_id != ProgramId::None) + glUseProgram(program_data->programs[program_id]); + active_program = program_id; + } +} + +int RenderInterface_GL3::GetUniformLocation(UniformId uniform_id) const +{ + return program_data->uniforms.Get(active_program, uniform_id); +} + +void RenderInterface_GL3::SubmitTransformUniform(Rml::Vector2f translation) +{ + static_assert((size_t)ProgramId::Count < MaxNumPrograms, "Maximum number of programs exceeded."); + const size_t program_index = (size_t)active_program; + + if (program_transform_dirty.test(program_index)) + { + glUniformMatrix4fv(GetUniformLocation(UniformId::Transform), 1, false, transform.data()); + program_transform_dirty.set(program_index, false); + } + + glUniform2fv(GetUniformLocation(UniformId::Translate), 1, &translation.x); + + Gfx::CheckGLError("SubmitTransformUniform"); +} + +RenderInterface_GL3::RenderLayerStack::RenderLayerStack() +{ + fb_postprocess.resize(4); +} + +RenderInterface_GL3::RenderLayerStack::~RenderLayerStack() +{ + DestroyFramebuffers(); +} + +Rml::LayerHandle RenderInterface_GL3::RenderLayerStack::PushLayer() +{ + RMLUI_ASSERT(layers_size <= (int)fb_layers.size()); + + if (layers_size == (int)fb_layers.size()) + { + // All framebuffers should share a single stencil buffer. + GLuint shared_depth_stencil = (fb_layers.empty() ? 0 : fb_layers.front().depth_stencil_buffer); + + fb_layers.push_back(Gfx::FramebufferData{}); + Gfx::CreateFramebuffer(fb_layers.back(), width, height, RMLUI_NUM_MSAA_SAMPLES, Gfx::FramebufferAttachment::DepthStencil, + shared_depth_stencil); + } + + layers_size += 1; + return GetTopLayerHandle(); +} + +void RenderInterface_GL3::RenderLayerStack::PopLayer() +{ + RMLUI_ASSERT(layers_size > 0); + layers_size -= 1; +} + +const Gfx::FramebufferData& RenderInterface_GL3::RenderLayerStack::GetLayer(Rml::LayerHandle layer) const +{ + RMLUI_ASSERT((size_t)layer < (size_t)layers_size); + return fb_layers[layer]; +} + +const Gfx::FramebufferData& RenderInterface_GL3::RenderLayerStack::GetTopLayer() const +{ + return GetLayer(GetTopLayerHandle()); +} + +Rml::LayerHandle RenderInterface_GL3::RenderLayerStack::GetTopLayerHandle() const +{ + RMLUI_ASSERT(layers_size > 0); + return static_cast<Rml::LayerHandle>(layers_size - 1); +} + +void RenderInterface_GL3::RenderLayerStack::SwapPostprocessPrimarySecondary() +{ + std::swap(fb_postprocess[0], fb_postprocess[1]); +} + +void RenderInterface_GL3::RenderLayerStack::BeginFrame(int new_width, int new_height) +{ + RMLUI_ASSERT(layers_size == 0); + + if (new_width != width || new_height != height) + { + width = new_width; + height = new_height; + + DestroyFramebuffers(); + } + + PushLayer(); +} + +void RenderInterface_GL3::RenderLayerStack::EndFrame() +{ + RMLUI_ASSERT(layers_size == 1); + PopLayer(); +} + +void RenderInterface_GL3::RenderLayerStack::DestroyFramebuffers() +{ + RMLUI_ASSERTMSG(layers_size == 0, "Do not call this during frame rendering, that is, between BeginFrame() and EndFrame()."); + + for (Gfx::FramebufferData& fb : fb_layers) + Gfx::DestroyFramebuffer(fb); + + fb_layers.clear(); + + for (Gfx::FramebufferData& fb : fb_postprocess) + Gfx::DestroyFramebuffer(fb); +} + +const Gfx::FramebufferData& RenderInterface_GL3::RenderLayerStack::EnsureFramebufferPostprocess(int index) +{ + RMLUI_ASSERT(index < (int)fb_postprocess.size()) + Gfx::FramebufferData& fb = fb_postprocess[index]; + if (!fb.framebuffer) + Gfx::CreateFramebuffer(fb, width, height, 0, Gfx::FramebufferAttachment::None, 0); + return fb; +} + +const Rml::Matrix4f& RenderInterface_GL3::GetTransform() const +{ + return transform; +} + +void RenderInterface_GL3::ResetProgram() +{ + UseProgram(ProgramId::None); +} + +bool RmlGL3::Initialize(Rml::String* out_message) +{ +#if defined(UNBOX_RMLUI_GLES) + if (out_message) + *out_message = "Started native OpenGL ES 3.2 renderer."; +#elif defined(RMLUI_PLATFORM_EMSCRIPTEN) + if (out_message) + *out_message = "Started Emscripten WebGL renderer."; +#elif defined(__ANDROID__) + if (out_message) + *out_message = "Started OpenGL ES 3 renderer."; +#elif !defined RMLUI_GL3_CUSTOM_LOADER + const int gl_version = gladLoaderLoadGL(); + if (gl_version == 0) + { + if (out_message) + *out_message = "Failed to initialize OpenGL context."; + return false; + } + + if (out_message) + *out_message = Rml::CreateString("Loaded OpenGL %d.%d.", GLAD_VERSION_MAJOR(gl_version), GLAD_VERSION_MINOR(gl_version)); +#endif + + return true; +} + +void RmlGL3::Shutdown() +{ +#if !defined(UNBOX_RMLUI_GLES) && !defined(RMLUI_PLATFORM_EMSCRIPTEN) && !defined(__ANDROID__) && !defined(RMLUI_GL3_CUSTOM_LOADER) + gladLoaderUnloadGL(); +#endif +} |
