[Refactor] Implement batched shape rendering.

This mirrors the previous refactor of the TextRenderer. All calls to
draw_rect and draw_triangle now buffer vertex data (position and color).
A begin()/flush() system is introduced to manage batching.
This commit is contained in:
Ritchie Cunningham 2025-10-05 00:44:23 +01:00
parent d992cb54bf
commit d37f632344
17 changed files with 217 additions and 91 deletions

View File

@ -1,8 +1,8 @@
#version 330 core
out vec4 FragColor;
uniform vec3 objectColor;
in vec3 ourColor;
void main() {
FragColor = vec4(objectColor, 1.0);
FragColor = vec4(ourColor, 1.0);
}

View File

@ -1,8 +1,12 @@
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
uniform mat4 projection;
out vec3 ourColor;
void main() {
gl_Position = projection * vec4(aPos.x, aPos.y, 0.0, 1.0);
ourColor = aColor;
}

View File

@ -13,8 +13,10 @@
#include "ui/i_window_content.h"
#include "ui/ui_window.h"
#include "ui/editor.h"
#include <SDL3/SDL_events.h>
#include <ui/main_menu.h>
#include <ui/boot_sequence.h>
#include "debug/debug_overlay.h"
void GameState::_init_desktop(void) {
_desktop = std::make_unique<Desktop>(_screen_width, _screen_height, _network.get());
@ -46,7 +48,8 @@ GameState::GameState(void) :
_current_screen(Screen::MAIN_MENU),
_screen_width(0),
_screen_height(0),
_is_single_player(false) {}
_is_single_player(false),
_show_debug_overlay(false) {_debug_overlay = std::make_unique<DebugOverlay>();}
GameState::~GameState(void) = default;
@ -77,6 +80,11 @@ void GameState::start_single_player_now(int screen_width, int screen_height) {
}
void GameState::handle_event(SDL_Event* event, int screen_width, int screen_height) {
if(event->type == SDL_EVENT_KEY_DOWN && event->key.key == SDLK_D &&
(event->key.mod & SDL_KMOD_CTRL)) {
_show_debug_overlay = !_show_debug_overlay;
return; /* Consume the event. */
}
switch(_current_screen) {
case Screen::MAIN_MENU:
if(_main_menu) {
@ -94,7 +102,8 @@ void GameState::handle_event(SDL_Event* event, int screen_width, int screen_heig
}
}
void GameState::update(float dt) {
void GameState::update(float dt, int draw_calls, int shape_verts, int text_verts) {
_debug_overlay->update(dt, draw_calls, shape_verts, text_verts);
switch(_current_screen) {
case Screen::MAIN_MENU: {
if(!_main_menu) break;
@ -221,4 +230,8 @@ void GameState::render(const RenderContext& context) {
}
break;
}
if(_show_debug_overlay) {
_debug_overlay->render(context.ui_renderer);
}
}

View File

@ -4,6 +4,7 @@
#include "gfx/types.h"
class DebugOverlay;
class ClientNetwork;
class Desktop;
class BootSequence;
@ -26,7 +27,7 @@ public:
void init(int screen_width, int screen_height);
void start_single_player_now(int screen_width, int screen_height);
void handle_event(SDL_Event* event, int screen_width, int screen_height);
void update(float dt);
void update(float dt, int draw_calls, int shape_verts, int text_verts);
void render(const RenderContext& context);
private:
@ -34,6 +35,8 @@ private:
std::unique_ptr<Desktop> _desktop;
std::unique_ptr<BootSequence> _boot_sequence;
std::unique_ptr<MainMenu> _main_menu;
std::unique_ptr<DebugOverlay> _debug_overlay;
bool _show_debug_overlay;
Screen _current_screen;
int _screen_width;
int _screen_height;

View File

@ -1,6 +1,7 @@
#include <GL/glew.h>
#include "shape_renderer.h"
#include "debug/debug_stats.h"
#include "math/math.h"
ShapeRenderer::ShapeRenderer(unsigned int screen_width, unsigned int screen_height) {
@ -12,67 +13,70 @@ ShapeRenderer::ShapeRenderer(unsigned int screen_width, unsigned int screen_heig
_shape_shader->use();
_shape_shader->set_mat4("projection", _projection);
/* Configure VAO/VBO. */
/* Configure VAO/VBO for batch rendering. */
glGenVertexArrays(1, &_vao);
glGenBuffers(1, &_vbo);
glBindVertexArray(_vao);
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 2, NULL, GL_DYNAMIC_DRAW);
glBufferData(GL_ARRAY_BUFFER, sizeof(ShapeVertex) * MAX_SHAPE_VERTICES, nullptr, GL_DYNAMIC_DRAW);
/* Position attribute. */
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), 0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(ShapeVertex), (void*)0);
/* Colour attribute. */
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(ShapeVertex), (void*)offsetof(ShapeVertex, r));
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
_vertices.reserve(MAX_SHAPE_VERTICES);
}
ShapeRenderer::~ShapeRenderer(void) {
delete _shape_shader;
}
void ShapeRenderer::draw_rect(int x, int y, int width, int height, const Color& color) {
_shape_shader->use();
_shape_shader->set_vec3("objectColor", color.r, color.g, color.b);
void ShapeRenderer::begin(void) {
_vertices.clear();
}
void ShapeRenderer::flush(void) {
if(_vertices.empty()) return;
_shape_shader->use();
glBindVertexArray(_vao);
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, _vertices.size() * sizeof(ShapeVertex), _vertices.data());
DebugStats::draw_calls++;
DebugStats::shape_vertices += _vertices.size();
glDrawArrays(GL_TRIANGLES, 0, _vertices.size());
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
void ShapeRenderer::draw_rect(int x, int y, int width, int height, const Color& color) {
float x_f = (float)x;
float y_f = (float)y;
float w_f = (float)width;
float h_f = (float)height;
/* This is ugly :) */
float vertices[6][2] = {
{ x_f, y_f + h_f },
{ x_f, y_f },
{ x_f + w_f, y_f },
_vertices.push_back({x_f, y_f+h_f, color.r, color.g, color.b});
_vertices.push_back({x_f, y_f, color.r, color.g, color.b});
_vertices.push_back({x_f+w_f, y_f, color.r, color.g, color.b});
{ x_f, y_f + h_f },
{ x_f + w_f, y_f },
{x_f + w_f, y_f + h_f }
};
glBindVertexArray(_vao);
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
_vertices.push_back({x_f, y_f+h_f, color.r, color.g, color.b});
_vertices.push_back({x_f+w_f, y_f, color.r, color.g, color.b});
_vertices.push_back({x_f+w_f, y_f+h_f, color.r, color.g, color.b});
}
void ShapeRenderer::draw_triangle(int x1, int y1, int x2, int y2, int x3,
int y3, const Color& color) {
_shape_shader->use();
_shape_shader->set_vec3("objectColor", color.r, color.g, color.b);
float vertices[3][2] = {
{(float)x1, (float)y1},
{(float)x2, (float)y2},
{(float)x3, (float)y3},
};
glBindVertexArray(_vao);
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
/*
* We're overwriting the buffer content here, this is fine for just one triangle,
* but don't do it for everything ;)
*/
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
_vertices.push_back({(float)x1, (float)y1, color.r, color.g, color.b});
_vertices.push_back({(float)x2, (float)y2, color.r, color.g, color.b});
_vertices.push_back({(float)x3, (float)y3, color.r, color.g, color.b});
}

View File

@ -1,13 +1,26 @@
#pragma once
#include <vector>
#include "shader.h"
#include "types.h"
struct ShapeVertex {
float x, y, r, g, b;
};
const int MAX_SHAPES_PER_BATCH = 10000;
const int VERTICES_PER_RECT = 6;
const int MAX_SHAPE_VERTICES = MAX_SHAPES_PER_BATCH * VERTICES_PER_RECT;
class ShapeRenderer {
public:
ShapeRenderer(unsigned int screen_width, unsigned int screen_height);
~ShapeRenderer(void);
void begin(void);
void flush(void);
void draw_rect(int x, int y, int width, int height, const Color& color);
void draw_triangle(int x1, int y1, int x2, int y2, int x3, int y3,
const Color& color);
@ -15,5 +28,6 @@ public:
private:
Shader* _shape_shader;
unsigned int _vao, _vbo;
std::vector<ShapeVertex> _vertices;
float _projection[16];
};

View File

@ -5,6 +5,7 @@
#include <freetype/freetype.h>
#include "txt_renderer.h"
#include "debug/debug_stats.h"
#include "math/math.h"
TextRenderer::TextRenderer(unsigned int screen_width, unsigned int screen_height) {
@ -141,6 +142,9 @@ void TextRenderer::flush(void) {
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, _vertices.size() * sizeof(TextVertex), _vertices.data());
DebugStats::draw_calls++;
DebugStats::text_vertices += _vertices.size();
glDrawArrays(GL_TRIANGLES, 0, _vertices.size());
glBindBuffer(GL_ARRAY_BUFFER, 0);

View File

@ -12,6 +12,7 @@
#include "ui/ui_renderer.h"
#include "gfx/types.h"
#include "ui/cursor_manager.h"
#include "debug/debug_stats.h"
const int SCREEN_WIDTH = 1280;
const int SCREEN_HEIGHT = 720;
@ -101,6 +102,9 @@ int main(int argc, char** argv) {
bool running = true;
while(running) {
/* Reset per-frame stats. */
DebugStats::reset();
/* Event handling. */
SDL_Event event;
while(SDL_PollEvent(&event)) {
@ -116,8 +120,6 @@ int main(int argc, char** argv) {
/* Clamp dt to avoid large jumps. */
if(dt > 0.1f) dt = 0.1f;
game_state->update(dt);
Uint32 current_time = SDL_GetTicks();
if(current_time - last_blink_time > 500) { /* Every 500ms. */
show_cursor = !show_cursor;
@ -135,6 +137,13 @@ int main(int argc, char** argv) {
};
game_state->render(context);
/* Update game state after rendering so stats are for the frame just rendered.
* I know this is unusual, but given the text based nature of the game
* we won't see a negative impact, and this is better than a large refactor ;)
*/
game_state->update(dt, DebugStats::draw_calls, DebugStats::shape_vertices,
DebugStats::text_vertices);
/* It's really odd to call it SwapWindow now, rather than SwapBuffer. */
SDL_GL_SwapWindow(window);
}

View File

@ -60,9 +60,10 @@ Desktop::Desktop(int screen_width, int screen_height, ClientNetwork* network) {
Desktop::~Desktop(void) {}
void Desktop::add_window(std::unique_ptr<UIWindow> window) {
UIWindow* window_ptr = window.get();
_windows.push_back(std::move(window));
/* Focus new window. */
_set_focused_window(_windows.back().get());
_taskbar->add_window(window_ptr);
_set_focused_window(window_ptr);
}
void Desktop::_set_focused_window(UIWindow* window) {
@ -92,31 +93,37 @@ void Desktop::_set_focused_window(UIWindow* window) {
}
void Desktop::handle_event(SDL_Event* event, int screen_width, int screen_height) {
/* Let focused window handle all events first. */
if(_focused_window) {
_focused_window->handle_event(event, screen_width, screen_height, _taskbar->get_height());
}
if(event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
int mouse_x = event->button.x;
int mouse_y = event->button.y;
/* If click is on taskbar, ignore it on the down-event. Let the up-event handle it. */
if(_taskbar->is_point_inside(mouse_x, mouse_y)) {
return;
}
/* When launcher open, check for outside click to close it. */
if(_launcher_is_open && !_launcher->is_point_inside(mouse_x, mouse_y, screen_height)) {
_launcher_is_open = false;
return;
}
/* If click no on focused window, find which window should receive focus. */
/* If click not on focused window, find which window should receive focus. */
bool click_on_window = false;
for(int i = _windows.size()-1; i >= 0; --i) {
if(_windows[i].get()->is_point_inside(mouse_x, mouse_y)) {
if(!_windows[i]->is_minimized()) {
_set_focused_window(_windows[i].get());
/* Top most window. Focus it. */
UIWindow* target = _windows[i].get();
if(!target->is_minimized()) {
_set_focused_window(target);
/* Pass event down to newly focused window to handle dragging, etc. */
target->handle_event(event, screen_width, screen_height, _taskbar->get_height());
}
click_on_window = true;
break;
}
}
/* Click wasn't on a window. Unfocus. */
if(!click_on_window) {
_set_focused_window(nullptr);
}
@ -139,7 +146,7 @@ void Desktop::handle_event(SDL_Event* event, int screen_width, int screen_height
_launcher_is_open = false;
}
} else {
UIWindow* clicked_window = _taskbar->handle_event(event, screen_height, _windows);
UIWindow* clicked_window = _taskbar->handle_event(event, screen_height);
if(clicked_window) {
if(clicked_window == _focused_window && !clicked_window->is_minimized()) {
clicked_window->minimize();
@ -160,6 +167,11 @@ void Desktop::handle_event(SDL_Event* event, int screen_width, int screen_height
}
CursorManager::set_cursor(CursorType::ARROW);
}
/* Pass all other non-down-click events to focused window. */
if(_focused_window && event->type != SDL_EVENT_MOUSE_BUTTON_DOWN) {
_focused_window->handle_event(event, screen_width, screen_height, _taskbar->get_height());
}
}
void Desktop::update(float dt, int screen_width, int screen_height) {
@ -193,6 +205,7 @@ void Desktop::update(float dt, int screen_width, int screen_height) {
_windows.erase(std::remove_if(_windows.begin(), _windows.end(),
[this](const std::unique_ptr<UIWindow>& w) {
if (w->should_close()) {
_taskbar->remove_window(w.get());
if (w.get() == _focused_window) {
_focused_window = nullptr;
}
@ -243,7 +256,7 @@ void Desktop::render(const RenderContext& context) {
if(_launcher_is_open) {
_launcher->render(context.ui_renderer);
}
_taskbar->render(context.ui_renderer, _windows, _focused_window);
_taskbar->render(context.ui_renderer, _focused_window);
context.ui_renderer->flush_text();
}

View File

@ -22,6 +22,9 @@ void Launcher::render(UIRenderer* ui_renderer) {
const Color text_color = { 0.9f, 0.9f, 0.9f };
const Color hover_color = { 0.3f, 0.32f, 0.34f };
ui_renderer->begin_shapes();
ui_renderer->begin_text();
/* Note: y-coord is TOP of launcher menu. */
ui_renderer->draw_rect(_x, _y, _width, _height, bg_color);
@ -41,6 +44,9 @@ void Launcher::render(UIRenderer* ui_renderer) {
ui_renderer->render_text(app_name.c_str(), _x+10, item_y+20, text_color);
item_y += item_height;
}
ui_renderer->flush_shapes();
ui_renderer->flush_text();
}
std::string Launcher::handle_event(SDL_Event* event, int screen_height) {

View File

@ -101,6 +101,7 @@ void MainMenu::render(UIRenderer* ui_renderer) {
ui_renderer->flush_text();
/* Pass 2: Buttons. */
ui_renderer->begin_shapes();
ui_renderer->begin_text();
/* Button colours. */
@ -124,6 +125,7 @@ void MainMenu::render(UIRenderer* ui_renderer) {
ui_renderer->render_text(button.label.c_str(), text_x, button.rect.y + 32, text_color);
}
ui_renderer->flush_shapes();
ui_renderer->flush_text();
}

View File

@ -73,8 +73,11 @@ void MenuBar::handle_event(SDL_Event* event, int window_x, int window_y) {
}
void MenuBar::render_bar(UIRenderer* ui_renderer, int x, int y, int width) {
const Color bg_color = {0.15f, 0.17f, 0.19f};
const Color text_color = {0.9f, 0.9f, 0.9f};
const Color bg_color = { 0.15f, 0.17f, 0.19f };
const Color text_color = { 0.9f, 0.9f, 0.9f };
ui_renderer->begin_shapes();
ui_renderer->begin_text();
ui_renderer->draw_rect(x, y, width, _height, bg_color);
@ -85,6 +88,9 @@ void MenuBar::render_bar(UIRenderer* ui_renderer, int x, int y, int width) {
menu_x += menu_width;
}
ui_renderer->flush_shapes();
ui_renderer->flush_text();
}
void MenuBar::render_dropdown(UIRenderer* ui_renderer, int x, int y, int width ) {
@ -93,6 +99,9 @@ void MenuBar::render_dropdown(UIRenderer* ui_renderer, int x, int y, int width )
const Color bg_color = { 0.15f, 0.17f, 0.19f };
const Color text_color = { 0.9f, 0.9f, 0.9f };
ui_renderer->begin_shapes();
ui_renderer->begin_text();
int menu_x = x + (_open_menu_index*60);
int item_y = y + _height;
int item_height = 30;
@ -102,4 +111,7 @@ void MenuBar::render_dropdown(UIRenderer* ui_renderer, int x, int y, int width )
ui_renderer->render_text(item.label.c_str(), menu_x+10, item_y+20, text_color);
item_y += item_height;
}
ui_renderer->flush_shapes();
ui_renderer->flush_text();
}

View File

@ -1,10 +1,11 @@
#include "taskbar.h"
#include <algorithm>
#include <memory>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <sstream>
#include "taskbar.h"
#include "ui/ui_renderer.h"
#include "ui/ui_window.h"
@ -17,15 +18,28 @@ Taskbar::Taskbar(int screen_width, int screen_height) {
Taskbar::~Taskbar(void) {}
void Taskbar::render(UIRenderer* ui_renderer,
const std::vector<std::unique_ptr<UIWindow>>& windows,
UIWindow* focused_window) {
void Taskbar::add_window(UIWindow* window) {
_buttons.push_back({window, window->get_title()});
}
void Taskbar::remove_window(UIWindow* window) {
_buttons.erase(
std::remove_if(_buttons.begin(), _buttons.end(),
[window](const TaskbarButton& btn) {
return btn.window == window;
}),
_buttons.end());
}
void Taskbar::render(UIRenderer* ui_renderer, UIWindow* focused_window) {
const Color taskbar_color = { 0.1f, 0.12f, 0.14f };
const Color button_color = { 0.2f, 0.22f, 0.24f };
const Color button_focused_color = { 0.3f, 0.32f, 0.34f };
const Color button_text_color = { 0.9f, 0.9f, 0.9f };
ui_renderer->begin_shapes();
ui_renderer->begin_text();
ui_renderer->draw_rect(0, _y_pos, _width, _height, taskbar_color);
/* Draw start button. */
@ -38,14 +52,14 @@ void Taskbar::render(UIRenderer* ui_renderer,
int padding = 5;
int x_offset = _start_button_width + padding;
for(const auto& window : windows) {
bool is_focused = (window.get() == focused_window);
for(const auto& button: _buttons) {
bool is_focused = (button.window == focused_window);
ui_renderer->draw_rect(x_offset, _y_pos + padding, button_width,
_height - (padding * 2),
is_focused ? button_focused_color : button_color);
/* TODO: Truncate text when too long. */
ui_renderer->render_text(window->get_title().c_str(), x_offset + 10,
ui_renderer->render_text(button.title.c_str(), x_offset + 10,
_y_pos + 20, button_text_color);
x_offset += button_width + padding;
}
@ -57,10 +71,12 @@ void Taskbar::render(UIRenderer* ui_renderer,
std::string time_str = ss.str();
ui_renderer->render_text(time_str.c_str(), _width-50, _y_pos+20, button_text_color);
ui_renderer->flush_shapes();
ui_renderer->flush_text();
}
UIWindow* Taskbar::handle_event(SDL_Event* event, int screen_height,
const std::vector<std::unique_ptr<UIWindow>>& windows) {
UIWindow* Taskbar::handle_event(SDL_Event* event, int screen_height) {
if(event->type == SDL_EVENT_MOUSE_BUTTON_UP) {
int mouse_x = event->button.x;
int mouse_y = event->button.y;
@ -68,10 +84,10 @@ UIWindow* Taskbar::handle_event(SDL_Event* event, int screen_height,
int button_width = 150;
int padding = 5;
int x_offset = _start_button_width + padding;
for(const auto& window : windows) {
for(const auto& button : _buttons) {
if(mouse_x >= x_offset && mouse_x <= x_offset + button_width &&
mouse_y >= _y_pos && mouse_y <= _y_pos + _height) {
return window.get(); /* Return clicked window. */
return button.window; /* Return clicked window. */
}
x_offset += button_width + padding;
}
@ -92,3 +108,7 @@ bool Taskbar::is_start_button_clicked(SDL_Event* event, int screen_height) {
int Taskbar::get_height(void) const {
return _height;
}
bool Taskbar::is_point_inside(int x, int y) const {
return (y >= _y_pos && y<= _y_pos + _height);
}

View File

@ -1,27 +1,33 @@
#pragma once
#include <SDL3/SDL_events.h>
#include <string>
#include <vector>
#include <memory>
class UIWindow;
class UIRenderer;
struct TaskbarButton {
UIWindow* window;
std::string title;
};
class Taskbar {
public:
Taskbar(int screen_width, int screen_height);
~Taskbar(void);
void render(UIRenderer* ui_renderer,
const std::vector<std::unique_ptr<UIWindow>>& windows,
UIWindow* focused_window);
void add_window(UIWindow* window);
void remove_window(UIWindow* window);
UIWindow* handle_event(SDL_Event* event, int screen_height,
const std::vector<std::unique_ptr<UIWindow>>& windows);
void render(UIRenderer* ui_renderer, UIWindow* focused_window);
UIWindow* handle_event(SDL_Event* event, int screen_height);
bool is_start_button_clicked(SDL_Event* event, int screen_height);
bool is_point_inside(int x, int y) const;
int get_height(void) const;
private:
std::vector<TaskbarButton> _buttons;
int _width;
int _height;
int _y_pos;

View File

@ -38,6 +38,16 @@ void UIRenderer::flush_text(void) {
_txt_renderer->flush();
}
void UIRenderer::begin_shapes(void) {
if(!_shape_renderer) return;
_shape_renderer->begin();
}
void UIRenderer::flush_shapes(void) {
if(!_shape_renderer) return;
_shape_renderer->flush();
}
TextRenderer* UIRenderer::get_text_renderer(void) {
return _txt_renderer;
}

View File

@ -20,6 +20,9 @@ public:
void begin_text(void);
void flush_text(void);
void begin_shapes(void);
void flush_shapes(void);
/* Expose underlying text renderer for things like width calculation. */
TextRenderer* get_text_renderer(void);

View File

@ -83,6 +83,7 @@ bool UIWindow::is_point_inside(int x, int y) {
void UIWindow::render(const RenderContext& context) {
int title_bar_height = 30;
context.ui_renderer->begin_shapes();
context.ui_renderer->begin_text();
/* Define colours. */
@ -121,6 +122,8 @@ void UIWindow::render(const RenderContext& context) {
context.ui_renderer->draw_triangle(corner_x, corner_y - 10, corner_x - 10,
corner_y, corner_x, corner_y, resize_handle_color);
/* Flush the shapes and text for the window frame and title bar. */
context.ui_renderer->flush_shapes();
context.ui_renderer->flush_text();
if(_content) {