#include <glad/glad_compat.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <vector>
#include <cmath>
#include <type_traits>
#include <chrono>
GLFWAPI GLFWwindow* createWindow(int width, int height, const char* title) {
GLFWwindow* window = nullptr;
glfwSetErrorCallback([](int /*error_code*/, const char* description) {
std::cerr << description << std::endl;
std::exit(EXIT_FAILURE);
});
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, true);
window = glfwCreateWindow(width, height, title, NULL, NULL);
if (!window) {
std::cerr << "Failed to create GLFW window" << std::endl;
glfwTerminate();
std::exit(EXIT_FAILURE);
}
// Make the OpenGL context for this window be the currently associated context for this thread.
glfwMakeContextCurrent(window);
// Load the OpenGL API function pointers.
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cerr << "Failed to initialize GLAD" << std::endl;
glfwDestroyWindow(window);
glfwTerminate();
std::exit(EXIT_FAILURE);
}
return window;
}
void checkShaderProgram(GLuint shader_program) {
GLint status = GL_FALSE;
glGetProgramiv(shader_program, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
GLchar info_log[4096];
glGetProgramInfoLog(shader_program, sizeof(info_log), NULL, info_log);
std::cerr << info_log << std::endl;
std::exit(EXIT_FAILURE);
}
};
const GLchar* shader_version_source = R"(
// Start every shader with the GLSL version.
#version 460 compatibility // OpenGL 4.6 Compatibility Profile
)";
const GLchar* vertex_output_source = R"(
// Define a common struct that will be used to pass data from the vertex shader to the fragment shader.
struct VSOutput {
vec4 color;
};
)";
struct ComputeShader {
GLuint program;
GLint height_uniform;
GLint width_uniform;
GLint frame_uniform;
};
struct FragmentShader {
GLuint program;
};
ComputeShader setupComputeShader() {
const GLchar* compute_shader_source = R"(
// Compute shader to generate a rotation matrix.
layout(local_size_x = 100, local_size_y = 1, local_size_z = 1) in;
uniform uint height, width, frame;
layout(binding = 1, std430) writeonly buffer ssbo { vec2 animations[]; };
void main() {
uint linear_index = gl_WorkGroupID.x * gl_WorkGroupSize.x + gl_LocalInvocationID.x;
if (linear_index >= height * width) {
return;
}
uint num_meshes = height, mesh = linear_index / width;
uint num_instances = width, instance = linear_index % width;
float instance_ratio = float(instance) / num_instances;
float mesh_ratio = float(mesh) / num_meshes;
float angle = 2*3.1415926f * ((mesh_ratio) + (instance_ratio)) + frame*0.01 + linear_index / float(num_meshes*num_instances);
vec2 offset = 1*vec2(sin(angle) * (mesh_ratio), cos(angle) * (instance_ratio));
animations[linear_index] = offset;
}
)";
const GLchar* compute_shader_sources[]{ shader_version_source, compute_shader_source };
GLuint compute_program = glCreateShaderProgramv(GL_COMPUTE_SHADER, (GLsizei)std::size(compute_shader_sources), compute_shader_sources);
checkShaderProgram(compute_program);
GLint height_uniform = glGetUniformLocation(compute_program, "height");
GLint width_uniform = glGetUniformLocation(compute_program, "width");
GLint frame_uniform = glGetUniformLocation(compute_program, "frame");
return { compute_program, height_uniform, width_uniform, frame_uniform };
}
FragmentShader setupFragmentShader() {
GLuint fragment_program;
const GLchar* fragment_shader_source = R"(
// Interpolated output from the vertex shader.
in VSOutput vsOutput;
// Just one output value. Automatically goes to the render target.
out vec4 outColor;
void main()
{
// output rgba = interpolated vertex rgba
outColor = vsOutput.color;
}
)";
const GLchar* fragment_shader_sources[]{ shader_version_source, vertex_output_source, fragment_shader_source };
fragment_program = glCreateShaderProgramv(GL_FRAGMENT_SHADER, (GLsizei)std::size(fragment_shader_sources), fragment_shader_sources);
checkShaderProgram(fragment_program);
return { fragment_program };
}
enum Mode {
MultiDrawElementsIndirect = 0,
DrawElementsInstancedBaseVertexBaseInstance,
DrawElementsInstanced,
DrawElements,
BeginEnd,
num_modes
};
Mode mode = MultiDrawElementsIndirect;
void setDrawMode(Mode new_mode, GLFWwindow* window) {
mode = new_mode;
const char* mode_name{};
switch (mode) {
case MultiDrawElementsIndirect: mode_name = "glMultiDrawElementsIndirect"; break;
case DrawElementsInstancedBaseVertexBaseInstance: mode_name = "glDrawElementsInstancedBaseVertexBaseInstance"; break;
case DrawElementsInstanced: mode_name = "glDrawElementsInstanced"; break;
case DrawElements: mode_name = "glDrawElements"; break;
case BeginEnd: mode_name = "glBegin/glEnd"; break;
default: mode_name = "Unknown"; break;
}
std::cout << "Switched to mode: " << mode_name << std::endl;
glfwSetWindowTitle(window, (std::string("Lesson 2: A History of Draw Calls - ") + mode_name).c_str());
}
int main()
{
GLFWwindow* window = createWindow(1024, 1024, "Lesson 5: A History of Draw Calls");
struct Vec3 {
GLfloat x;
GLfloat y;
GLfloat z;
};
struct RGBA8888 {
GLubyte r;
GLubyte g;
GLubyte b;
GLubyte a;
};
struct Vertex {
Vec3 position;
RGBA8888 color;
};
struct Triangle {
GLushort v0;
GLushort v1;
GLushort v2;
};
struct SolidQuad {
Vertex verts[4]{
{{-0.01f,-0.01f,0.0f}, {255, 255, 255, 255}},
{{+0.01f,-0.01f,0.0f}, {255, 255, 255, 255}},
{{+0.01f,+0.01f,0.0f}, {255, 255, 255, 255}},
{{-0.01f,+0.01f,0.0f}, {255, 255, 255, 255}},
};
Triangle tris[2]{
{0, 1, 2},
{0, 2, 3}
};
SolidQuad(float r, float g, float b, float a) {
for (auto& v : verts) {
v.color.r = static_cast<GLubyte>(r * 255);
v.color.g = static_cast<GLubyte>(g * 255);
v.color.b = static_cast<GLubyte>(b * 255);
v.color.a = static_cast<GLubyte>(a * 255);
}
}
};
std::vector<Vertex> cpu_vertex_data;
std::vector<Triangle> cpu_index_data;
struct Mesh {
unsigned int instance_count = 0;
unsigned int base_index = 0;
unsigned int index_count = 0;
unsigned int base_vertex = 0;
unsigned int vertex_count = 0;
};
std::vector<Mesh> meshes;
constexpr size_t num_rows = 47, num_meshes = 100000, max_instances = 10;
GLuint mesh_buffer = 0;
GLint verts_buffer_offset = 0;
glCreateBuffers(1, &mesh_buffer);
{
auto generateMesh = [](Vec3 bottom_left, Vec3 top_right, RGBA8888 c, GLuint num_rows, unsigned int num_instances, unsigned int base_vertex, unsigned int base_index, Vertex* dest_verts, GLushort* dest_indices)->Mesh {
Vec3 row_delta{
(top_right.x - bottom_left.x),
(top_right.y - bottom_left.y) / static_cast<float>(num_rows),
0.0f
};
Mesh mesh{
num_instances,
base_index,
num_rows * 6,
base_vertex,
(num_rows + 1) * 2
};
Vec3 v0 = bottom_left;
Vec3 v1 = { bottom_left.x + row_delta.x, bottom_left.y, bottom_left.z };
RGBA8888 c0 = c;
RGBA8888 c1 = c;
dest_verts[mesh.base_vertex + 0] = Vertex{ v0, c0 };
dest_verts[mesh.base_vertex + 1] = Vertex{ v1, c1 };
for (unsigned int row = 0; row < num_rows; row++) {
Vec3 v2 = { v0.x, v0.y + row_delta.y, v0.z };
Vec3 v3 = { v1.x, v1.y + row_delta.y, v1.z };
RGBA8888 c2 = { GLubyte(c0.r + 1), GLubyte(c0.g + 2), GLubyte(c0.b + 3), c0.a };
RGBA8888 c3 = { GLubyte(c1.r + 1), GLubyte(c1.g + 2), GLubyte(c1.b + 3), c1.a };
dest_verts[mesh.base_vertex + (row + 1) * 2 + 0] = Vertex{ v2, c2 };
dest_verts[mesh.base_vertex + (row + 1) * 2 + 1] = Vertex{ v3, c3 };
dest_indices[mesh.base_index + row * 6 + 0] = static_cast<GLushort>((row + 0) * 2 + 0);
dest_indices[mesh.base_index + row * 6 + 1] = static_cast<GLushort>((row + 0) * 2 + 1);
dest_indices[mesh.base_index + row * 6 + 2] = static_cast<GLushort>((row + 1) * 2 + 1);
dest_indices[mesh.base_index + row * 6 + 3] = static_cast<GLushort>((row + 0) * 2 + 0);
dest_indices[mesh.base_index + row * 6 + 4] = static_cast<GLushort>((row + 1) * 2 + 1);
dest_indices[mesh.base_index + row * 6 + 5] = static_cast<GLushort>((row + 1) * 2 + 0);
v0 = v2;
v1 = v3;
c0 = c2;
c1 = c3;
};
return mesh;
};
size_t num_verts = num_meshes * (num_rows + 1) * 2;
size_t num_indices = num_meshes * num_rows * 6;
size_t vertex_bytes = num_verts * sizeof(Vertex);
size_t index_bytes = num_indices * sizeof(GLushort);
size_t total_bytes = vertex_bytes + index_bytes;
cpu_vertex_data.assign(num_verts, Vertex{});
cpu_index_data.assign(num_indices, Triangle{});
GLushort* dest_indices = (GLushort*)(cpu_index_data.data());
verts_buffer_offset = (GLint)index_bytes;
Vertex* dest_verts = (Vertex*)(cpu_vertex_data.data());
Mesh previous_mesh{};
for (int i = 0; i < num_meshes; i++) {
float width = 32.0f / num_meshes;
Vec3 bottom_left{
(i - 1)* width,
-0.125f,
0.0f
};
Vec3 top_right{
(i + 1) * width,
0.0125f,
0.0f
};
previous_mesh = meshes.emplace_back(
generateMesh(
bottom_left,
top_right,
RGBA8888{ GLubyte(i*3), GLubyte(i * 2), GLubyte(i * 1), 255 },
num_rows,
max_instances,
previous_mesh.base_vertex + previous_mesh.vertex_count,
previous_mesh.base_index + previous_mesh.index_count,
dest_verts,
dest_indices
)
);
}
for (Mesh& mesh : meshes) {
for (unsigned int i = 0; i < mesh.index_count; ++i) {
((GLushort*)cpu_index_data.data())[mesh.base_index + i] += mesh.base_vertex;
}
mesh.base_vertex = 0;
}
glNamedBufferStorage(mesh_buffer, total_bytes, nullptr, GL_MAP_WRITE_BIT);
{
GLubyte* mapped = (GLubyte*)glMapNamedBuffer(mesh_buffer, GL_WRITE_ONLY);
memcpy(mapped, cpu_index_data.data(), index_bytes);
memcpy(mapped + verts_buffer_offset, cpu_vertex_data.data(), vertex_bytes);
glUnmapNamedBuffer(mesh_buffer);
}
}
// https://www.khronos.org/opengl/wiki/Vertex_Rendering/Rendering_Failure
// "The index buffer binding is stored within the VAO. If no VAO is bound, then you cannot bind a buffer object to GL_ELEMENT_ARRAY_BUFFER."
GLuint vertex_array_object = 0;
glCreateVertexArrays(1, &vertex_array_object);
const GLuint mesh_buffer_binding = 0, position_attrib = 10, color_attrib = 11;
glVertexArrayAttribBinding(vertex_array_object, position_attrib, mesh_buffer_binding);
glVertexArrayAttribFormat(vertex_array_object, position_attrib, 3, GL_FLOAT, GL_FALSE, offsetof(Vertex, position));
glEnableVertexArrayAttrib(vertex_array_object, position_attrib);
glVertexArrayAttribBinding(vertex_array_object, color_attrib, mesh_buffer_binding);
glVertexArrayAttribFormat(vertex_array_object, color_attrib, 4, GL_UNSIGNED_BYTE, GL_TRUE, offsetof(Vertex, color));
glEnableVertexArrayAttrib(vertex_array_object, color_attrib);
struct Vec2 {
GLfloat x;
GLfloat y;
};
GLuint animation_buffer = 0;
glCreateBuffers(1, &animation_buffer);
constexpr GLuint total_instances = num_meshes * max_instances;
glNamedBufferStorage(animation_buffer, sizeof(Vec2[total_instances]), nullptr, GL_DYNAMIC_STORAGE_BIT);
struct VertexShader {
GLuint program;
GLint manual_draw_id_uniform;
GLint manual_base_instance_uniform;
GLint manual_instance_uniform;
GLint rotation_uniform;
} vertex_shader;
{
const GLchar* vertex_shader_source = R"(
layout (location = 10) in vec3 position;
layout (location = 11) in vec4 color;
layout (binding = 1, std430) readonly buffer ssbo2 { vec2 animations[]; };
uniform uint manual_draw_id;
uniform uint manual_base_instance;
uniform uint manual_instance;
uniform mat2x2 rotation_matrix;
out gl_PerVertex {
vec4 gl_Position;
};
// `vec4 gl_Position;` is implicitly defined in every vertex shader.
// For additional outputs to the fragment shader, we need an `out` variable.
out VSOutput vsOutput;
void main() {
uint base_instance = gl_BaseInstance + manual_base_instance;
uint instance_id = gl_InstanceID + manual_instance;
vec3 ur_position = position + gl_Vertex.xyz;
vec4 ur_color = color + gl_Color;
gl_Position.xy = rotation_matrix * ur_position.xy + animations[base_instance + instance_id];
gl_Position.z = position.z;
gl_Position.w = 1.0f;
vsOutput.color = ur_color;
}
)";
const GLchar* vertex_shader_sources[]{ shader_version_source, vertex_output_source, vertex_shader_source };
GLuint vertex_program = glCreateShaderProgramv(GL_VERTEX_SHADER, (GLsizei)std::size(vertex_shader_sources), vertex_shader_sources);
checkShaderProgram(vertex_program);
GLint manual_draw_id_uniform = glGetUniformLocation(vertex_program, "manual_draw_id");
GLint manual_base_instance_uniform = glGetUniformLocation(vertex_program, "manual_base_instance");
GLint manual_instance_uniform = glGetUniformLocation(vertex_program, "manual_instance");
GLint rotation_uniform = glGetUniformLocation(vertex_program, "rotation_matrix");
vertex_shader = { vertex_program, manual_draw_id_uniform, manual_base_instance_uniform, manual_instance_uniform, rotation_uniform };
}
ComputeShader compute_shader = setupComputeShader();
FragmentShader fragment_shader = setupFragmentShader();
GLuint pipeline = 0;
glGenProgramPipelines(1, &pipeline);
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vertex_shader.program);
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fragment_shader.program);
constexpr GLuint anim_binding = 1;
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, anim_binding, animation_buffer);
// https://registry.khronos.org/OpenGL-Refpages/gl4/html/glMultiDrawArraysIndirect.xhtml
struct DrawElementsIndirectCommand {
unsigned int count;
unsigned int instanceCount;
unsigned int firstIndex;
int baseVertex;
unsigned int baseInstance;
};
std::vector<DrawElementsIndirectCommand> cpu_commands_vector(num_meshes, {});
GLuint command_buffer = 0;
glCreateBuffers(1, &command_buffer);
glNamedBufferStorage(command_buffer, cpu_commands_vector.size() * sizeof(DrawElementsIndirectCommand), nullptr, GL_DYNAMIC_STORAGE_BIT);
setDrawMode(mode, window);
glfwSetKeyCallback(window, [](GLFWwindow* window, int key, int scancode, int action, int mods) {
if (action == GLFW_PRESS && key == GLFW_KEY_SPACE) {
setDrawMode(Mode((mode + 1) % num_modes), window);
}
});
auto start_time = std::chrono::high_resolution_clock::now();
int start_frame = 0;
for (int frame = 0; !glfwWindowShouldClose(window); frame++) {
auto frame_time = std::chrono::high_resolution_clock::now();
if (frame_time - start_time > std::chrono::seconds(1)) {
float milliseconds_per_frame = std::chrono::duration<float, std::milli>(frame_time - start_time).count() / (frame - start_frame);
std::cout << "Average frame time over last " << (frame - start_frame) << " frames: " << milliseconds_per_frame << " ms (" << 1000.0f / milliseconds_per_frame << " fps)" << std::endl;
start_time = frame_time;
start_frame = frame;
}
glClearColor(0.0f, 0.0f, 0.5f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
constexpr GLuint anims_width = 10;
constexpr GLuint anims_height = total_instances / anims_width;
glProgramUniform1ui(compute_shader.program, compute_shader.width_uniform, anims_width);
glProgramUniform1ui(compute_shader.program, compute_shader.height_uniform, anims_height);
glProgramUniform1ui(compute_shader.program, compute_shader.frame_uniform, frame);
glUseProgram(compute_shader.program);
glDispatchCompute(anims_height, 1, 1);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
glUseProgram(0);
struct Mat2x2 {
GLfloat m00, m01;
GLfloat m10, m11;
};
float angle = frame * -0.003f;
Mat2x2 rotation_matrix = {
std::cos(angle), -std::sin(angle),
std::sin(angle), std::cos(angle)
};
glProgramUniformMatrix2fv(vertex_shader.program, vertex_shader.rotation_uniform, 1, false, (const float*)&rotation_matrix);
glProgramUniform1ui(vertex_shader.program, vertex_shader.manual_base_instance_uniform, 0);
glProgramUniform1ui(vertex_shader.program, vertex_shader.manual_instance_uniform, 0);
glBindProgramPipeline(pipeline);
glBindVertexArray(vertex_array_object);
const GLsizei bind_count = 1;
const GLuint bind_buffers[bind_count] = { mesh_buffer };
const GLintptr bind_offsets[bind_count] = { verts_buffer_offset };
const GLsizei bind_strides[bind_count] = { sizeof(Vertex) };
glVertexArrayVertexBuffers(vertex_array_object, mesh_buffer_binding, bind_count, bind_buffers, bind_offsets, bind_strides);
glVertexArrayElementBuffer(vertex_array_object, mesh_buffer);
GLuint base_instance = 0;
GLsizei draw_count = 0;
glfwSwapInterval(0); // Disable vsync.
for (const Mesh& mesh : meshes) {
cpu_commands_vector[draw_count++] = { mesh.index_count, mesh.instance_count, mesh.base_index, (int)mesh.base_vertex, base_instance };
base_instance += mesh.instance_count;
}
glColor4f(0, 0, 0, 0);
glVertex4f(0, 0, 0, 0);
switch (mode) {
case MultiDrawElementsIndirect:{
glNamedBufferSubData(command_buffer, 0, cpu_commands_vector.size() * sizeof(cpu_commands_vector[0]), cpu_commands_vector.data());
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, command_buffer);
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_SHORT, 0, draw_count, 0);
} break;
case DrawElementsInstancedBaseVertexBaseInstance: {
for (DrawElementsIndirectCommand& command : cpu_commands_vector) {
glDrawElementsInstancedBaseVertexBaseInstance(
GL_TRIANGLES,
command.count,
GL_UNSIGNED_SHORT,
(const GLvoid*)(command.firstIndex * sizeof(GLushort)),
command.instanceCount,
command.baseVertex,
command.baseInstance
);
}
} break;
case DrawElementsInstanced: {
GLuint draw_id = 0, base_instance = 0;
for (DrawElementsIndirectCommand& command : cpu_commands_vector) {
glProgramUniform1ui(vertex_shader.program, vertex_shader.manual_draw_id_uniform, draw_id);
glProgramUniform1ui(vertex_shader.program, vertex_shader.manual_base_instance_uniform, base_instance);
glDrawElementsInstanced(
GL_TRIANGLES,
command.count,
GL_UNSIGNED_SHORT,
(const GLvoid*)(command.firstIndex * sizeof(GLushort)),
command.instanceCount
);
draw_id++;
base_instance += command.instanceCount;
}
} break;
case DrawElements: {
GLuint draw_id = 0, base_instance = 0;
for (DrawElementsIndirectCommand& command : cpu_commands_vector) {
glProgramUniform1ui(vertex_shader.program, vertex_shader.manual_draw_id_uniform, draw_id);
glProgramUniform1ui(vertex_shader.program, vertex_shader.manual_base_instance_uniform, base_instance);
for (unsigned int instance = 0; instance < command.instanceCount; ++instance) {
glProgramUniform1ui(vertex_shader.program, vertex_shader.manual_instance_uniform, instance);
glDrawElements(
GL_TRIANGLES,
command.count,
GL_UNSIGNED_SHORT,
(const GLvoid*)(command.firstIndex * sizeof(GLushort))
);
base_instance++;
}
draw_id++;
}
} break;
case BeginEnd: {
GLuint draw_id = 0, base_instance = 0;
for (DrawElementsIndirectCommand& command : cpu_commands_vector) {
glProgramUniform1ui(vertex_shader.program, vertex_shader.manual_draw_id_uniform, draw_id);
glProgramUniform1ui(vertex_shader.program, vertex_shader.manual_base_instance_uniform, base_instance);
for (unsigned int instance = 0; instance < command.instanceCount; ++instance) {
glProgramUniform1ui(vertex_shader.program, vertex_shader.manual_instance_uniform, instance);
glBegin(GL_TRIANGLES);
const GLushort* indices = ((GLushort*)cpu_index_data.data()) + command.firstIndex;
for (unsigned int i = 0; i < command.count; ++i) {
GLushort index = indices[i];
const Vertex& v = cpu_vertex_data[index];
glColor4ubv((const GLubyte*)&v.color);
glVertex3fv((const GLfloat*)&v.position);
}
glEnd();
base_instance++;
}
draw_id++;
}
} break;
default:
break;
}
glfwSwapBuffers(window);
glfwPollEvents();
}
// We could just let process termination clean everything up for us.
// But, let's manually clean up our resources just to be explicit.
glDeleteVertexArrays(1, &vertex_array_object);
glDeleteProgramPipelines(1, &pipeline);
glDeleteProgram(compute_shader.program);
glDeleteProgram(fragment_shader.program);
glDeleteProgram(vertex_shader.program);
GLuint buffers_to_delete[] = { command_buffer, animation_buffer, mesh_buffer };
glDeleteBuffers(std::size(buffers_to_delete), buffers_to_delete);
// Shut down and clean up everything we did with GLFW.
glfwTerminate();
// Exit the program.
return 0;
}