/*
* This source file is part of RmlUi, the HTML/CSS Interface Middleware
*
* For the latest information, see http://github.com/mikke89/RmlUi
*
* Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
* Copyright (c) 2019-2023 The RmlUi Team, and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#include "RmlUi_Renderer_GL3.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#if defined(RMLUI_PLATFORM_WIN32) && !defined(__MINGW32__)
// function call missing argument list
#pragma warning(disable : 4551)
// unreferenced local function has been removed
#pragma warning(disable : 4505)
#endif
#if defined RMLUI_PLATFORM_EMSCRIPTEN
#define RMLUI_SHADER_HEADER_VERSION "#version 300 es\nprecision highp float;\n"
#include
#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.
static constexpr int NUM_MSAA_SAMPLES = 2;
#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
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 ids = {};
};
using Programs = EnumArray;
using VertShaders = EnumArray;
using FragShaders = EnumArray;
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(id) << 32) | static_cast(uniform); }
Rml::UnorderedMap 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, Depth, 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 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)
{
#ifdef RMLUI_PLATFORM_EMSCRIPTEN
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);
#ifndef RMLUI_PLATFORM_EMSCRIPTEN
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);
const GLenum internal_format = (attachment == FramebufferAttachment::DepthStencil ? GL_DEPTH24_STENCIL8 : GL_DEPTH_COMPONENT24);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, internal_format, width, height);
}
const GLenum attachment_type = (attachment == FramebufferAttachment::DepthStencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment_type, 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 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();
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)
{
viewport_width = Rml::Math::Max(width, 1);
viewport_height = Rml::Math::Max(height, 1);
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);
#ifndef RMLUI_PLATFORM_EMSCRIPTEN
// 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 backbuffer
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 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);
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 vertices, Rml::Span 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 vertical axis of the rectangle, and move its origin to the vertically opposite side of the viewport.
/// @note Changes 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;
const bool clear_stencil = (operation == ClipMaskOperation::Set || operation == ClipMaskOperation::SetInverse);
if (clear_stencil)
{
// @performance Increment the reference value instead of clearing each time.
glClear(GL_STENCIL_BUFFER_BIT);
}
GLint stencil_test_value = 0;
glGetIntegerv(GL_STENCIL_REF, &stencil_test_value);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glStencilFunc(GL_ALWAYS, GLint(1), GLuint(-1));
switch (operation)
{
case ClipMaskOperation::Set:
{
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
stencil_test_value = 1;
}
break;
case ClipMaskOperation::SetInverse:
{
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
stencil_test_value = 0;
}
break;
case ClipMaskOperation::Intersect:
{
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
stencil_test_value += 1;
}
break;
}
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 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 image_dest_buffer(new byte[image_size]);
byte* image_dest = image_dest_buffer.get();
// Targa is BGR, swap to RGB, flip Y axis, and convert to premultiplied alpha.
for (long y = 0; y < header.height; y++)
{
long read_index = y * header.width * color_mode;
long write_index = ((header.imageDescriptor & 32) != 0) ? read_index : (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 source_data, Rml::Vector2i source_dimensions)
{
GLuint texture_id = 0;
glGenTextures(1, &texture_id);
if (texture_id == 0)
{
Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to generate texture.");
return false;
}
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 (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);
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 = 0.5f * Rml::Get(parameters, "radius", 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(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(filter);
}
enum class CompiledShaderType { Invalid = 0, Gradient, Creation };
struct CompiledShader {
CompiledShaderType type;
// Gradient
ShaderGradientFunction gradient_function;
Rml::Vector2f p;
Rml::Vector2f v;
Rml::Vector stop_positions;
Rml::Vector 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();
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(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(shader_handle);
const CompiledShaderType type = shader.type;
const Gfx::CompiledGeometryData& geometry = *reinterpret_cast(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(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(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 filter_handles)
{
for (const Rml::CompiledFilterHandle filter_handle : filter_handles)
{
const CompiledFilter& filter = *reinterpret_cast(filter_handle);
const FilterType type = filter.type;
switch (type)
{
case FilterType::Passthrough:
{
UseProgram(ProgramId::Passthrough);
glBlendFunc(GL_CONSTANT_ALPHA, GL_ZERO);
glBlendColor(0.0f, 0.0f, 0.0f, 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::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 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;
Rml::TextureHandle render_texture = GenerateTexture({}, bounds.Size());
if (!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, (GLuint)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 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(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, 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(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;
}
bool RmlGL3::Initialize(Rml::String* out_message)
{
#if defined RMLUI_PLATFORM_EMSCRIPTEN
if (out_message)
*out_message = "Started Emscripten WebGL 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 RMLUI_PLATFORM_EMSCRIPTEN && !defined RMLUI_GL3_CUSTOM_LOADER
gladLoaderUnloadGL();
#endif
}