From d081108ea535e380d98c54b6726eff660f0cdc81 Mon Sep 17 00:00:00 2001 From: Cristian Gora Date: Wed, 28 Oct 2026 21:23:41 +0100 Subject: [PATCH] Add GL error handling, resource fallbacks and GPU timing; speed up lighting Bug fixes: - Release static GL objects (UBOs, logo texture) before the context is destroyed via new Renderer::shutdown() - Fix max point/spot light warnings never firing - Show frame time in ms instead of seconds - Size the viewport framebuffer in physical pixels on HiDPI displays - Stop double-flipping OBJ UVs; add flip_uvs option to load_obj for models with a top-left UV origin (backpack) Build: - Track header dependencies (-MMD -MP) so header edits trigger rebuilds - Create build/ automatically Error handling: - Add gldebug module: KHR_debug output routed to the logger in debug builds, object labels, and error/status string helpers - Validate inputs and check GL errors when creating textures, meshes, framebuffers, uniform buffers and shaders; FrameBuffer::resize() and Renderer::init() now report failure Resources: - Add ResourceCache: shares textures and shaders by path and substitutes fallbacks (magenta checkerboard texture, unlit shader, cube mesh) for assets that fail to load; specular maps are optional - load_obj never fails, a broken model becomes a fallback cube - Renderer draws submeshes with broken materials using the fallback material instead of skipping them - Add Texture::create_from_pixels, Shader::create_from_source and Mesh::create_cube - Cache uniform locations in Shader, looked up with string_view so hits don't allocate; missing uniforms warn once Performance: - Add CpuTimer and GpuTimer (GL_TIME_ELAPSED, non-blocking readback) and a Performance section in the UI with averaged overlay numbers - Sample material textures once per fragment and apply material colors after accumulating lights - Pass camera position through CameraBlock instead of inverting the view matrix per fragment - Compute the normal matrix on the CPU instead of per vertex - Skip spot light work for fragments outside the outer cone --- Makefile | 28 +++-- README.md | 6 +- assets/textures/debugempty.png | Bin 231 -> 0 bytes src/camera.hpp | 13 ++- src/config.hpp | 7 +- src/entity.hpp | 2 +- src/fan.hpp | 3 + src/framebuffer.cpp | 56 +++++++++- src/framebuffer.hpp | 11 +- src/gldebug.cpp | 128 ++++++++++++++++++++++ src/gldebug.hpp | 27 +++++ src/light.hpp | 9 +- src/loader.cpp | 36 +++--- src/loader.hpp | 6 +- src/logger.hpp | 4 +- src/main.cpp | 194 +++++++++++++++++++-------------- src/material.hpp | 7 +- src/mesh.cpp | 95 +++++++++++++++- src/mesh.hpp | 7 ++ src/renderer.cpp | 78 +++++++------ src/renderer.hpp | 15 ++- src/resourcecache.cpp | 189 ++++++++++++++++++++++++++++++++ src/resourcecache.hpp | 56 ++++++++++ src/scene.hpp | 2 + src/shader.cpp | 104 ++++++++++++++---- src/shader.hpp | 36 ++++-- src/shaders/default.frag | 128 ++++++++++++---------- src/shaders/default.vert | 5 +- src/texture.cpp | 36 ++++-- src/texture.hpp | 7 ++ src/timer.cpp | 130 ++++++++++++++++++++++ src/timer.hpp | 64 +++++++++++ src/transform.hpp | 6 +- src/uniformbuffer.cpp | 22 +++- src/uniformbuffer.hpp | 5 +- src/utils.hpp | 3 + src/vertex.hpp | 1 + src/window.cpp | 8 ++ src/window.hpp | 15 ++- 39 files changed, 1275 insertions(+), 274 deletions(-) delete mode 100644 assets/textures/debugempty.png create mode 100644 src/gldebug.cpp create mode 100644 src/gldebug.hpp create mode 100644 src/resourcecache.cpp create mode 100644 src/resourcecache.hpp create mode 100644 src/timer.cpp create mode 100644 src/timer.hpp diff --git a/Makefile b/Makefile index 7c136be..8d9942a 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ CXX := clang++ CXXFLAGS := -Wall -Wextra -std=c++17 CC := clang CFLAGS := -Wall -Wextra +DEPFLAGS := -MMD -MP INCLUDES := -I./src/ \ -I./vendor/glad/include/ \ @@ -17,14 +18,17 @@ TARGET := build/fan OBJS := build/camera.o \ build/framebuffer.o \ + build/gldebug.o \ build/loader.o \ build/main.o \ build/material.o \ build/mesh.o \ build/renderer.o \ + build/resourcecache.o \ build/scene.o \ build/shader.o \ build/texture.o \ + build/timer.o \ build/transform.o \ build/uniformbuffer.o \ build/utils.o \ @@ -33,30 +37,31 @@ OBJS := build/camera.o \ build/stb_image.o \ build/tiny_obj_loader.o \ -IMGUI_OBJS := build/imgui.o build/imgui_demo.o build/imgui_draw.o build/imgui_tables.o build/imgui_widgets.o \ +IMGUI_OBJS := build/imgui.o build/imgui_demo.o build/imgui_draw.o \ + build/imgui_tables.o build/imgui_widgets.o \ build/imgui_impl_glfw.o build/imgui_impl_opengl3.o .PHONY: all clean all: $(TARGET) -$(TARGET): $(OBJS) $(IMGUI_OBJS) +$(TARGET): $(OBJS) $(IMGUI_OBJS) | build $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -build/%.o: src/%.cpp - $(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@ +build/%.o: src/%.cpp | build + $(CXX) $(CXXFLAGS) $(DEPFLAGS) $(INCLUDES) -c $< -o $@ -build/glad.o: vendor/glad/src/gl.c +build/glad.o: vendor/glad/src/gl.c | build $(CC) $(CFLAGS) -I./vendor/glad/include/ -c $< -o $@ -build/stb_image.o: vendor/stb/stb_image.h +build/stb_image.o: vendor/stb/stb_image.h | build $(CC) $(CFLAGS) -DSTB_IMAGE_IMPLEMENTATION -xc -c $< -o $@ -build/tiny_obj_loader.o: vendor/tinyobjloader/tiny_obj_loader.h +build/tiny_obj_loader.o: vendor/tinyobjloader/tiny_obj_loader.h | build $(CXX) $(CXXFLAGS) -DTINYOBJLOADER_IMPLEMENTATION -xc++ -c $< -o $@ define imgui_rule -build/$(1): vendor/imgui/$(2) +build/$(1): vendor/imgui/$(2) | build $(CXX) $(CXXFLAGS) -I./vendor/imgui/ -c $$< -o $$@ endef @@ -68,5 +73,10 @@ $(eval $(call imgui_rule,imgui_widgets.o,imgui_widgets.cpp)) $(eval $(call imgui_rule,imgui_impl_glfw.o,backends/imgui_impl_glfw.cpp)) $(eval $(call imgui_rule,imgui_impl_opengl3.o,backends/imgui_impl_opengl3.cpp)) +build: + mkdir -p build + clean: - rm -f build/*.o $(TARGET) + rm -f build/*.o build/*.d $(TARGET) + +-include $(OBJS:.o=.d) diff --git a/README.md b/README.md index 7792959..e864485 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ system-wide**, the rest of the dependencies are already included. After that, building comes down to just running the included Makefile. ```console -$ mkdir build $ make # optionally pass -j$(nproc) ``` @@ -51,6 +50,7 @@ flowchart TB Renderer --> Camera Renderer --> Scene Renderer -->|Updates| UniformBuffer + Renderer -->|Owns| ResourceCache end subgraph World @@ -89,6 +89,8 @@ flowchart TB Material --> Shader Material --> Texture UniformBuffer -->|Global data| Shader + ResourceCache -.->|Caches, falls back| Texture + ResourceCache -.->|Caches, falls back| Shader end end @@ -101,7 +103,7 @@ flowchart TB classDef loading fill:#d97706,stroke:#92400e,color:#fff classDef platform fill:#64748b,stroke:#334155,color:#fff - class Renderer,Scene,Camera,Framebuffer application + class Renderer,Scene,Camera,Framebuffer,ResourceCache application class Entity,Transform,Mesh,SubMesh,Material,LightEnvironment world class UniformBuffer,Shader,Texture,Vertices,Indices,DirectionalLight,PointLight,SpotLight data class Loader,tinyobjloader,stb_image loading diff --git a/assets/textures/debugempty.png b/assets/textures/debugempty.png deleted file mode 100644 index 8e61b3417b34b0a8c05fc563ded122092b1dc660..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 231 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7#Nv>)VXbLJAo8ifKQ0)e}?}+=KN(MpMcaH zPZ!6Kid%1QEfi!h;BejeZhwj40!x*cf*Bp_@2~z{dHKH$P!$3ks9<1N05TB@4v4{6 iVEPL?CEVMLD7qPAY7cKZ&gM21B mesh; - Rc material{}; // Optional material override + Rc material{}; // Optional, replaces the material of every SubMesh Entity(Rc mesh, Rc material = nullptr) : mesh(mesh) diff --git a/src/fan.hpp b/src/fan.hpp index 6ad1ef9..1c032b1 100644 --- a/src/fan.hpp +++ b/src/fan.hpp @@ -5,14 +5,17 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include +#include #include #include #include diff --git a/src/framebuffer.cpp b/src/framebuffer.cpp index 56b7ebe..b86676f 100644 --- a/src/framebuffer.cpp +++ b/src/framebuffer.cpp @@ -1,6 +1,7 @@ #include #include "framebuffer.hpp" +#include "gldebug.hpp" #include "logger.hpp" namespace Fan @@ -8,6 +9,13 @@ namespace Fan Rc FrameBuffer::create(GLsizei width, GLsizei height) { + if (!is_valid_size(width, height)) { + Log::error() << "Could not create framebuffer: invalid size " << width << "x" << height; + return nullptr; + } + + gl_clear_errors(); + GLuint id, texture, render_buffer; glGenFramebuffers(1, &id); @@ -31,14 +39,24 @@ Rc FrameBuffer::create(GLsizei width, GLsizei height) GL_RENDERBUFFER, render_buffer); GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - if (status != GL_FRAMEBUFFER_COMPLETE) { + GLenum err = gl_pop_error(); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glBindTexture(GL_TEXTURE_2D, 0); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + if (status != GL_FRAMEBUFFER_COMPLETE || err != GL_NO_ERROR) { glDeleteRenderbuffers(1, &render_buffer); glDeleteTextures(1, &texture); glDeleteFramebuffers(1, &id); - Log::error() << "Could not create incomplete framebuffer"; + Log::error() << "Could not create framebuffer: " << gl_framebuffer_status_string(status) + << ", " << gl_error_string(err); return nullptr; } + set_gl_label(GL_FRAMEBUFFER, id, "FrameBuffer"); + set_gl_label(GL_TEXTURE, texture, "FrameBuffer color"); + set_gl_label(GL_RENDERBUFFER, render_buffer, "FrameBuffer depth/stencil"); + Log::info() << "Created framebuffer (" << width << "x" << height << ")"; return make_rc(id, width, height, texture, render_buffer); } @@ -97,8 +115,15 @@ void FrameBuffer::unbind() const glBindFramebuffer(GL_FRAMEBUFFER, 0); } -void FrameBuffer::resize(GLsizei width, GLsizei height) +bool FrameBuffer::resize(GLsizei width, GLsizei height) { + if (!is_valid_size(width, height)) { + Log::error() << "Could not resize framebuffer: invalid size " << width << "x" << height; + return false; + } + + gl_clear_errors(); + m_width = width; m_height = height; @@ -113,6 +138,31 @@ void FrameBuffer::resize(GLsizei width, GLsizei height) m_width, m_height); glBindTexture(GL_TEXTURE_2D, 0); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + // Check completeness without disturbing the caller's framebuffer binding + GLint previous_fbo = 0; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &previous_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, m_id); + GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + glBindFramebuffer(GL_FRAMEBUFFER, static_cast(previous_fbo)); + + GLenum err = gl_pop_error(); + if (status != GL_FRAMEBUFFER_COMPLETE || err != GL_NO_ERROR) { + Log::error() << "Could not resize framebuffer to " << width << "x" << height + << ": " << gl_framebuffer_status_string(status) + << ", " << gl_error_string(err); + return false; + } + + return true; +} + +bool FrameBuffer::is_valid_size(GLsizei width, GLsizei height) +{ + GLint max_size = 0; + glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &max_size); + return width > 0 && height > 0 && width <= max_size && height <= max_size; } } // namespace Fan diff --git a/src/framebuffer.hpp b/src/framebuffer.hpp index aecb6ff..29ca3b0 100644 --- a/src/framebuffer.hpp +++ b/src/framebuffer.hpp @@ -8,6 +8,8 @@ namespace Fan { +// Off-screen render target: an RGB8 color texture (see get_texture_id) and a +// depth/stencil renderbuffer. Sizes are in pixels class FrameBuffer { public: @@ -31,8 +33,11 @@ public: GLsizei get_height() const; void bind() const; - void unbind() const; - void resize(GLsizei width, GLsizei height); + void unbind() const; // Binds the default framebuffer (the window) + + // Reallocates the attachments, discarding their contents. Returns false if + // the new size is invalid or the framebuffer became incomplete + bool resize(GLsizei width, GLsizei height); private: GLuint m_id{}; @@ -41,6 +46,8 @@ private: GLuint m_texture{}; GLuint m_render_buffer{}; + + static bool is_valid_size(GLsizei width, GLsizei height); }; } // namespace Fan diff --git a/src/gldebug.cpp b/src/gldebug.cpp new file mode 100644 index 0000000..001aa20 --- /dev/null +++ b/src/gldebug.cpp @@ -0,0 +1,128 @@ +#include + +#include "gldebug.hpp" +#include "logger.hpp" + +namespace Fan +{ + +static const char* debug_source_string(GLenum source) +{ + switch (source) { + case GL_DEBUG_SOURCE_API: return "API"; + case GL_DEBUG_SOURCE_WINDOW_SYSTEM: return "Window system"; + case GL_DEBUG_SOURCE_SHADER_COMPILER: return "Shader compiler"; + case GL_DEBUG_SOURCE_THIRD_PARTY: return "Third party"; + case GL_DEBUG_SOURCE_APPLICATION: return "Application"; + default: return "Other"; + } +} + +static const char* debug_type_string(GLenum type) +{ + switch (type) { + case GL_DEBUG_TYPE_ERROR: return "error"; + case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: return "deprecated"; + case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: return "undefined behavior"; + case GL_DEBUG_TYPE_PORTABILITY: return "portability"; + case GL_DEBUG_TYPE_PERFORMANCE: return "performance"; + default: return "other"; + } +} + +static void GLAD_API_PTR debug_callback(GLenum source, GLenum type, GLuint id, + GLenum severity, GLsizei length, + const GLchar* message, const void* user_param) +{ + (void) length; + (void) user_param; + + LogStream (*log)() = Log::trace; + switch (severity) { + case GL_DEBUG_SEVERITY_HIGH: log = Log::error; break; + case GL_DEBUG_SEVERITY_MEDIUM: log = Log::warn; break; + case GL_DEBUG_SEVERITY_LOW: log = Log::debug; break; + default: break; + } + + // Output is synchronous: put a breakpoint here to see the offending call in the stack + log() << "OpenGL " << debug_source_string(source) << " " << debug_type_string(type) + << " (" << id << "): " << message; +} + +void enable_gl_debug_output() +{ + if (!GLAD_GL_KHR_debug) { + Log::warn() << "KHR_debug not supported, OpenGL debug output disabled"; + return; + } + + glEnable(GL_DEBUG_OUTPUT); + glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); + glDebugMessageCallback(debug_callback, nullptr); + + // Notifications are mostly driver chatter (buffer placement, shader recompiles...) + glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DEBUG_SEVERITY_NOTIFICATION, + 0, nullptr, GL_FALSE); + + Log::info() << "Enabled OpenGL debug output"; +} + +void set_gl_label(GLenum identifier, GLuint id, const std::string& label) +{ + if (!GLAD_GL_KHR_debug) { return; } + + // Labels must be shorter than GL_MAX_LABEL_LENGTH + GLint max_length = 0; + glGetIntegerv(GL_MAX_LABEL_LENGTH, &max_length); + GLsizei length = std::min(static_cast(label.size()), max_length - 1); + + glObjectLabel(identifier, id, length, label.c_str()); +} + +void gl_clear_errors() +{ + gl_pop_error(); +} + +GLenum gl_pop_error() +{ + GLenum first = glGetError(); + if (first == GL_NO_ERROR) { return first; } + + // Several error flags can be set at once, drain them so they aren't + // blamed on a later call (bounded in case the driver keeps reporting) + for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {} + + return first; +} + +const char* gl_error_string(GLenum error) +{ + switch (error) { + case GL_NO_ERROR: return "GL_NO_ERROR"; + case GL_INVALID_ENUM: return "GL_INVALID_ENUM"; + case GL_INVALID_VALUE: return "GL_INVALID_VALUE"; + case GL_INVALID_OPERATION: return "GL_INVALID_OPERATION"; + case GL_INVALID_FRAMEBUFFER_OPERATION: return "GL_INVALID_FRAMEBUFFER_OPERATION"; + case GL_OUT_OF_MEMORY: return "GL_OUT_OF_MEMORY"; + default: return "Unknown OpenGL error"; + } +} + +const char* gl_framebuffer_status_string(GLenum status) +{ + switch (status) { + case GL_FRAMEBUFFER_COMPLETE: return "GL_FRAMEBUFFER_COMPLETE"; + case GL_FRAMEBUFFER_UNDEFINED: return "GL_FRAMEBUFFER_UNDEFINED"; + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: return "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT"; + case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: return "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"; + case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: return "GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER"; + case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: return "GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER"; + case GL_FRAMEBUFFER_UNSUPPORTED: return "GL_FRAMEBUFFER_UNSUPPORTED"; + case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: return "GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE"; + default: return "Unknown framebuffer status"; + } +} + +} // namespace Fan diff --git a/src/gldebug.hpp b/src/gldebug.hpp new file mode 100644 index 0000000..a8f0637 --- /dev/null +++ b/src/gldebug.hpp @@ -0,0 +1,27 @@ +#ifndef FAN_GLDEBUG_HPP_ +#define FAN_GLDEBUG_HPP_ + +#include +#include + +namespace Fan +{ + +// Route driver messages to the logger through KHR_debug (no-op if unsupported) +void enable_gl_debug_output(); + +// Name a GL object so debug messages and tools like RenderDoc show it +void set_gl_label(GLenum identifier, GLuint id, const std::string& label); + +// Discard errors left over from earlier calls +void gl_clear_errors(); + +// Drain the error queue and return the first error (GL_NO_ERROR if none) +GLenum gl_pop_error(); + +const char* gl_error_string(GLenum error); +const char* gl_framebuffer_status_string(GLenum status); + +} // namespace Fan + +#endif // FAN_GLDEBUG_HPP_ diff --git a/src/light.hpp b/src/light.hpp index fa03956..6b86b63 100644 --- a/src/light.hpp +++ b/src/light.hpp @@ -7,9 +7,12 @@ namespace Fan { +// Attenuation of point and spot lights at distance d: +// 1 / (constant + linear * d + quadratic * d^2) + struct DirectionalLight { - glm::vec3 direction{-0.2f, -1.0f, -0.3f}; + glm::vec3 direction{-0.2f, -1.0f, -0.3f}; // Direction the light travels, normalized in the shader glm::vec3 ambient{0.05f, 0.05f, 0.05f}; glm::vec3 diffuse{0.4f, 0.4f, 0.4f}; @@ -34,6 +37,8 @@ struct SpotLight glm::vec3 position{0.0f, 0.0f, 0.0f}; glm::vec3 direction{0.0f, 0.0f, -1.0f}; + // Cosines (not angles) of the inner and outer cone half-angles. The light + // fades out between the two float cut_off{glm::cos(glm::radians(12.5f))}; float outer_cut_off{glm::cos(glm::radians(15.0f))}; @@ -49,6 +54,8 @@ struct SpotLight struct LightEnvironment { DirectionalLight directional_light; + + // Lights past MAX_POINT_LIGHTS / MAX_SPOT_LIGHTS are ignored by the renderer std::vector point_lights; std::vector spot_lights; }; diff --git a/src/loader.cpp b/src/loader.cpp index b17bf8c..39b6530 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -3,13 +3,15 @@ #include #include "loader.hpp" +#include "resourcecache.hpp" #include "logger.hpp" namespace Fan { struct VertexIndexHash { - size_t operator()(const tinyobj::index_t& index) const { + size_t operator()(const tinyobj::index_t& index) const + { return ((std::hash()(index.vertex_index) ^ (std::hash()(index.normal_index) << 1)) >> 1) ^ (std::hash()(index.texcoord_index) << 1); @@ -17,14 +19,22 @@ struct VertexIndexHash { }; struct VertexIndexEqual { - bool operator()(const tinyobj::index_t& a, const tinyobj::index_t& b) const { + bool operator()(const tinyobj::index_t& a, const tinyobj::index_t& b) const + { return a.vertex_index == b.vertex_index && a.normal_index == b.normal_index && a.texcoord_index == b.texcoord_index; } }; -Opt load_obj(const std::string& file_path, Rc shader) +// Magenta checkerboard cube standing in for a model that failed to load +static Entity create_fallback_entity() +{ + return Entity{ResourceCache::get_fallback_mesh(), ResourceCache::get_fallback_material()}; +} + +Entity load_obj(const std::string& file_path, Rc shader, + bool flip_uvs/* = false*/) { namespace fs = std::filesystem; @@ -42,7 +52,7 @@ Opt load_obj(const std::string& file_path, Rc shader) file_path.c_str(), base_dir.c_str()); if (!ok) { Log::error() << "Could not load " << file_path << ": " << err; - return std::nullopt; + return create_fallback_entity(); } std::vector> loaded_materials; @@ -58,16 +68,11 @@ Opt load_obj(const std::string& file_path, Rc shader) mat->specular_color = glm::vec3{mtl.specular[0], mtl.specular[1], mtl.specular[2]}; if (!mtl.diffuse_texname.empty()) { - mat->diffuse_map = Texture::create(base_dir + mtl.diffuse_texname); - if (!mat->diffuse_map) { - Log::warn() << "Could not create diffuse texture " << mtl.diffuse_texname; - } + mat->diffuse_map = ResourceCache::load_texture(base_dir + mtl.diffuse_texname); } if (!mtl.specular_texname.empty()) { - mat->specular_map = Texture::create(base_dir + mtl.specular_texname); - if (!mat->specular_map) { - Log::warn() << "Could not create specular texture" << mtl.specular_texname; - } + // Optional: without it the shader uses specular_color alone + mat->specular_map = ResourceCache::load_texture(base_dir + mtl.specular_texname, false); } loaded_materials.push_back(mat); @@ -111,10 +116,13 @@ Opt load_obj(const std::string& file_path, Rc shader) }; } + // Textures are already flipped on load (stbi_set_flip_vertically_on_load), + // which matches the OBJ convention of a bottom-left UV origin if (idx.texcoord_index >= 0) { + float v = attrib.texcoords[2 * idx.texcoord_index + 1]; vertex.texture_coords = { attrib.texcoords[2 * idx.texcoord_index + 0], - 1.0f - attrib.texcoords[2 * idx.texcoord_index + 1] + flip_uvs ? 1.0f - v : v }; } @@ -151,7 +159,7 @@ Opt load_obj(const std::string& file_path, Rc shader) auto mesh = Mesh::create(vertices, all_indices, submeshes); if (!mesh) { Log::error() << "Could not create mesh for " << file_path; - return std::nullopt; + return create_fallback_entity(); } Log::info() << "Loaded " << file_path << " (" diff --git a/src/loader.hpp b/src/loader.hpp index b53c163..21a5fca 100644 --- a/src/loader.hpp +++ b/src/loader.hpp @@ -9,7 +9,11 @@ namespace Fan { -Opt load_obj(const std::string& file_path, Rc shader); +// Never fails: a model that can't be loaded is replaced by a fallback cube. +// flip_uvs: for models authored with a top-left UV origin instead of the OBJ +// standard bottom-left (e.g. the LearnOpenGL backpack) +Entity load_obj(const std::string& file_path, Rc shader, + bool flip_uvs = false); } // namespace Fan diff --git a/src/logger.hpp b/src/logger.hpp index 26be458..97ffb87 100644 --- a/src/logger.hpp +++ b/src/logger.hpp @@ -19,6 +19,8 @@ enum class LogLevel Off, }; +// Prints one line per statement: the header on construction, the newline when +// the temporary is destroyed at the end of the full expression class LogStream { public: @@ -70,7 +72,7 @@ private: class Log { public: - static inline LogLevel level = LogLevel::Trace; + static inline LogLevel level = LogLevel::Trace; // Messages below this level are dropped static void set_level(LogLevel new_level) { level = new_level; } diff --git a/src/main.cpp b/src/main.cpp index 459db76..522c680 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,6 @@ +#include +#include +#include #include #include #include @@ -11,6 +14,34 @@ using namespace Fan; +// Ring buffer of per-frame measurements, for plotting and stable averages +struct MetricHistory +{ + static constexpr size_t SIZE = 120; + + std::array values{}; + size_t offset{}; // Index of the oldest value, where the next one is written + size_t count{}; + + void push(float value) + { + values[offset] = value; + offset = (offset + 1) % SIZE; + count = std::min(count + 1, SIZE); + } + + float average() const + { + if (count == 0) { return 0.0f; } + + float sum = 0.0f; + for (size_t i = 0; i < count; ++i) { + sum += values[(offset + SIZE - 1 - i) % SIZE]; + } + return sum / static_cast(count); + } +}; + struct UserData { Window& window; @@ -27,6 +58,15 @@ struct UserData bool vsync{true}; bool show_debug{}; + Rc logo{}; + + // Performance measurements of the scene pass (GPU timer is null if unsupported) + CpuTimer scene_cpu_timer{}; + Rc scene_gpu_timer{}; + MetricHistory frame_times{}; + MetricHistory scene_cpu_times{}; + MetricHistory scene_gpu_times{}; + std::vector> scenes{}; size_t selected_scene_index{}; }; @@ -36,10 +76,10 @@ void mouse_callback(double pos_x, double pos_y, void* user_data); void setup_dockspace(); void draw_panel_window(UserData& user_data); void draw_viewport_window(FrameBuffer& framebuffer, UserData& user_data); +void plot_metric(const char* label, const MetricHistory& history); void add_random_lights(Scene& scene, int point_light_count, int spot_light_count, const glm::vec3& box_min, const glm::vec3& box_max, unsigned int seed = std::random_device{}()); -std::vector get_cube_vertices(); template void overlay_text(float offset_x, float offset_y, ImVec4 color, const char* format, Args... args) @@ -63,7 +103,7 @@ int main() if (!window) return EXIT_FAILURE; window->set_vsync(true); - Renderer::init(); + if (!Renderer::init()) return EXIT_FAILURE; Camera camera{{6.0f, 3.0f, 1.0f}}; camera.fov = 60.0f; @@ -75,28 +115,25 @@ int main() /******************** Resource loading ********************/ - auto shader = Shader::create("./src/shaders/default.vert", "./src/shaders/default.frag"); - auto debug_shader = Shader::create("./src/shaders/debug.vert", "./src/shaders/debug.frag", false); + auto shader = ResourceCache::load_shader("./src/shaders/default.vert", "./src/shaders/default.frag"); + auto debug_shader = ResourceCache::load_shader("./src/shaders/debug.vert", "./src/shaders/debug.frag", false); - auto sponza = load_obj("./assets/sponza/sponza.obj", shader); + Entity sponza = load_obj("./assets/sponza/sponza.obj", shader); + Entity car = load_obj("./assets/audi_r8/Audi_R8_2017.obj", shader); + Entity backpack = load_obj("./assets/backpack/backpack.obj", shader, true); - auto car = load_obj("./assets/audi_r8/Audi_R8_2017.obj", shader); + auto cube_texture = ResourceCache::load_texture("./assets/textures/container.jpg"); + auto default_texture = ResourceCache::load_texture("./assets/textures/wall.jpg"); + user_data.logo = ResourceCache::load_texture("assets/logo/logo.png"); - auto backpack = load_obj("./assets/backpack/backpack.obj", shader); - - auto cube_mesh = Mesh::create(get_cube_vertices()); - auto cube_texture = Texture::create("./assets/textures/container.jpg"); - - auto default_mesh = Mesh::create(get_cube_vertices()); - auto default_texture = Texture::create("./assets/textures/debugempty.png"); + user_data.scene_gpu_timer = GpuTimer::create(); + auto cube_mesh = Mesh::create_cube(); auto framebuffer = FrameBuffer::create(window->get_width(), window->get_height()); - if (!shader || !debug_shader - || !backpack - || !cube_mesh || !cube_texture - || !default_mesh || !default_texture - || !framebuffer) { + // Built from memory or sized from the window, failing here means OpenGL is broken + if (!cube_mesh || !framebuffer) { + Renderer::shutdown(); return EXIT_FAILURE; } @@ -106,9 +143,9 @@ int main() Rc sponza_scene = make_rc(); user_data.scenes.push_back(sponza_scene); - sponza->transform.position = {0.0f, -0.5f, 0.0f}; - sponza->transform.scale = {0.008f, 0.008f, 0.008f}; - sponza_scene->add_entity(*sponza); + sponza.transform.position = {0.0f, -0.5f, 0.0f}; + sponza.transform.scale = {0.008f, 0.008f, 0.008f}; + sponza_scene->add_entity(sponza); add_random_lights(*sponza_scene, 4, 4, glm::vec3(-10.0f, 1.0f, -3.0f), glm::vec3(10.0f, 5.0f, 3.0f), @@ -118,7 +155,7 @@ int main() Rc car_scene = make_rc(); user_data.scenes.push_back(car_scene); - car_scene->add_entity(*car); + car_scene->add_entity(car); add_random_lights(*car_scene, 4, 4, glm::vec3(-10.0f, 1.0f, -3.0f), glm::vec3(10.0f, 5.0f, 3.0f), @@ -128,7 +165,7 @@ int main() Rc backpack_scene = make_rc(); user_data.scenes.push_back(backpack_scene); - backpack_scene->add_entity(*backpack); + backpack_scene->add_entity(backpack); auto default_material = make_rc(); default_material->shader = shader; @@ -175,6 +212,7 @@ int main() window->update(); user_data.delta_time = window->get_delta_time(); + user_data.frame_times.push(user_data.delta_time * 1000.0f); process_input(*window, camera, user_data); Renderer::set_clear_color({0.0f, 0.0f, 0.0f, 1.0f}); @@ -191,18 +229,30 @@ int main() int width = framebuffer->get_width(); int height = framebuffer->get_height(); framebuffer->bind(); + user_data.scene_cpu_timer.begin(); + if (user_data.scene_gpu_timer) { user_data.scene_gpu_timer->begin(); } + Renderer::set_viewport(0, 0, width, height); Renderer::clear(); Renderer::draw_scene(scene, camera, width, height); if (user_data.show_debug) { Renderer::draw_debug_wireframes(scene, camera, width, height, - debug_shader, default_mesh); + debug_shader, ResourceCache::get_fallback_mesh()); } + + if (user_data.scene_gpu_timer) { user_data.scene_gpu_timer->end(); } + user_data.scene_cpu_timer.end(); framebuffer->unbind(); + user_data.scene_cpu_times.push(user_data.scene_cpu_timer.get_elapsed_ms()); + if (user_data.scene_gpu_timer) { + user_data.scene_gpu_times.push(user_data.scene_gpu_timer->get_elapsed_ms()); + } + window->end(); } + Renderer::shutdown(); return EXIT_SUCCESS; } @@ -281,11 +331,9 @@ void setup_dockspace() void draw_panel_window(UserData& user_data) { - static auto logo = Texture::create("assets/logo/logo.png"); - ImGui::Begin("Panel"); - if (logo) ImGui::Image(logo->get_id(), {50.0f, 50.0f}, {0.0f, 1.0f}, {1.0f, 0.0f}); + if (user_data.logo) ImGui::Image(user_data.logo->get_id(), {50.0f, 50.0f}, {0.0f, 1.0f}, {1.0f, 0.0f}); ImGui::SameLine(); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (50.0f - ImGui::GetTextLineHeight()) * 0.5f); ImGui::Text("Fan Engine"); @@ -312,6 +360,17 @@ void draw_panel_window(UserData& user_data) ImGui::DragFloat3("Position", &user_data.camera.position.x, 0.1f, -20.0f, 20.0f, "%.1f"); ImGui::SliderFloat("FOV", &user_data.camera.fov, 30.0f, 100.0f, "%.1f"); + ImGui::SeparatorText("Performance"); + + ImGui::TextDisabled("Disable VSync for accurate frame times"); + plot_metric("Frame", user_data.frame_times); + plot_metric("Scene CPU", user_data.scene_cpu_times); + if (user_data.scene_gpu_timer) { + plot_metric("Scene GPU", user_data.scene_gpu_times); + } else { + ImGui::TextDisabled("Scene GPU: timer queries not supported"); + } + ImGui::SeparatorText("Key Bindings"); ImGui::Text("ESCAPE: Quit application"); @@ -330,23 +389,44 @@ void draw_viewport_window(FrameBuffer& framebuffer, UserData& user_data) ImVec2 size = ImGui::GetContentRegionAvail(); if (size.x > 0 && size.y > 0) { - if (size.x != framebuffer.get_width() || size.y != framebuffer.get_height()) { - framebuffer.resize(size.x, size.y); + // Size the framebuffer in physical pixels so HiDPI displays render at full resolution + ImVec2 scale = ImGui::GetIO().DisplayFramebufferScale; + GLsizei fb_width = static_cast(size.x * scale.x); + GLsizei fb_height = static_cast(size.y * scale.y); + if (fb_width != framebuffer.get_width() || fb_height != framebuffer.get_height()) { + framebuffer.resize(fb_width, fb_height); } ImGui::Image(framebuffer.get_texture_id(), size, ImVec2{0.0f, 1.0f}, ImVec2{1.0f, 0.0f}); } - // Overlays - float fps = user_data.window.get_delta_time() > 0.0f ? 1.0f / user_data.window.get_delta_time() : 0.0f; + // Overlays (averaged over the last MetricHistory::SIZE frames so they're readable) + float frame_ms = user_data.frame_times.average(); + float fps = frame_ms > 0.0f ? 1000.0f / frame_ms : 0.0f; overlay_text(20.0f, 35.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "%s", VERSION_STRING); overlay_text(20.0f, 52.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "FPS: %.1f", fps); - overlay_text(20.0f, 69.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "Frame time: %.2f ms", user_data.window.get_delta_time()); - if (user_data.mouse_grab) { overlay_text(20.0f, 86.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "%s", "INPUT LOCKED"); } + overlay_text(20.0f, 69.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "Frame time: %.2f ms", frame_ms); + overlay_text(20.0f, 86.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "Scene CPU: %.2f ms", user_data.scene_cpu_times.average()); + if (user_data.scene_gpu_timer) { + overlay_text(20.0f, 103.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "Scene GPU: %.2f ms", user_data.scene_gpu_times.average()); + } else { + overlay_text(20.0f, 103.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "%s", "Scene GPU: n/a"); + } + if (user_data.mouse_grab) { overlay_text(20.0f, 120.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "%s", "INPUT LOCKED"); } ImGui::End(); ImGui::PopStyleVar(); } +void plot_metric(const char* label, const MetricHistory& history) +{ + char overlay[32]; + std::snprintf(overlay, sizeof(overlay), "avg %.2f ms", history.average()); + + // Values are stored as a ring buffer, values_offset makes the plot start at the oldest + ImGui::PlotLines(label, history.values.data(), static_cast(history.values.size()), + static_cast(history.offset), overlay, 0.0f, FLT_MAX, ImVec2{0.0f, 40.0f}); +} + void add_random_lights(Scene& scene, int point_light_count, int spot_light_count, const glm::vec3& box_min, const glm::vec3& box_max, unsigned int seed/* = std::random_device{}()*/) @@ -417,51 +497,3 @@ void add_random_lights(Scene& scene, int point_light_count, int spot_light_count scene.light_environment.spot_lights.push_back(light); } } - -std::vector get_cube_vertices() -{ - return { - // Back Face - {{ 0.5f, -0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 0.0f, 0.0f}}, - {{-0.5f, -0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 1.0f, 0.0f}}, - {{-0.5f, 0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 1.0f, 1.0f}}, - {{-0.5f, 0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 1.0f, 1.0f}}, - {{ 0.5f, 0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 0.0f, 1.0f}}, - {{ 0.5f, -0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 0.0f, 0.0f}}, - // Front Face - {{-0.5f, -0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 0.0f, 0.0f}}, - {{ 0.5f, -0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 1.0f, 0.0f}}, - {{ 0.5f, 0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 1.0f, 1.0f}}, - {{ 0.5f, 0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 1.0f, 1.0f}}, - {{-0.5f, 0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 0.0f, 1.0f}}, - {{-0.5f, -0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 0.0f, 0.0f}}, - // Left Face - {{-0.5f, -0.5f, -0.5f}, {-1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f}}, - {{-0.5f, -0.5f, 0.5f}, {-1.0f, 0.0f, 0.0f}, { 1.0f, 0.0f}}, - {{-0.5f, 0.5f, 0.5f}, {-1.0f, 0.0f, 0.0f}, { 1.0f, 1.0f}}, - {{-0.5f, 0.5f, 0.5f}, {-1.0f, 0.0f, 0.0f}, { 1.0f, 1.0f}}, - {{-0.5f, 0.5f, -0.5f}, {-1.0f, 0.0f, 0.0f}, { 0.0f, 1.0f}}, - {{-0.5f, -0.5f, -0.5f}, {-1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f}}, - // Right Face - {{ 0.5f, -0.5f, 0.5f}, { 1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f}}, - {{ 0.5f, -0.5f, -0.5f}, { 1.0f, 0.0f, 0.0f}, { 1.0f, 0.0f}}, - {{ 0.5f, 0.5f, -0.5f}, { 1.0f, 0.0f, 0.0f}, { 1.0f, 1.0f}}, - {{ 0.5f, 0.5f, -0.5f}, { 1.0f, 0.0f, 0.0f}, { 1.0f, 1.0f}}, - {{ 0.5f, 0.5f, 0.5f}, { 1.0f, 0.0f, 0.0f}, { 0.0f, 1.0f}}, - {{ 0.5f, -0.5f, 0.5f}, { 1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f}}, - // Bottom Face - {{-0.5f, -0.5f, -0.5f}, { 0.0f, -1.0f, 0.0f}, { 0.0f, 1.0f}}, - {{ 0.5f, -0.5f, -0.5f}, { 0.0f, -1.0f, 0.0f}, { 1.0f, 1.0f}}, - {{ 0.5f, -0.5f, 0.5f}, { 0.0f, -1.0f, 0.0f}, { 1.0f, 0.0f}}, - {{ 0.5f, -0.5f, 0.5f}, { 0.0f, -1.0f, 0.0f}, { 1.0f, 0.0f}}, - {{-0.5f, -0.5f, 0.5f}, { 0.0f, -1.0f, 0.0f}, { 0.0f, 0.0f}}, - {{-0.5f, -0.5f, -0.5f}, { 0.0f, -1.0f, 0.0f}, { 0.0f, 1.0f}}, - // Top Face - {{-0.5f, 0.5f, 0.5f}, { 0.0f, 1.0f, 0.0f}, { 0.0f, 0.0f}}, - {{ 0.5f, 0.5f, 0.5f}, { 0.0f, 1.0f, 0.0f}, { 1.0f, 0.0f}}, - {{ 0.5f, 0.5f, -0.5f}, { 0.0f, 1.0f, 0.0f}, { 1.0f, 1.0f}}, - {{ 0.5f, 0.5f, -0.5f}, { 0.0f, 1.0f, 0.0f}, { 1.0f, 1.0f}}, - {{-0.5f, 0.5f, -0.5f}, { 0.0f, 1.0f, 0.0f}, { 0.0f, 1.0f}}, - {{-0.5f, 0.5f, 0.5f}, { 0.0f, 1.0f, 0.0f}, { 0.0f, 0.0f}} - }; -} diff --git a/src/material.hpp b/src/material.hpp index cbddd6d..08370fe 100644 --- a/src/material.hpp +++ b/src/material.hpp @@ -10,13 +10,16 @@ namespace Fan struct Material { Rc shader; - Rc diffuse_map; - Rc specular_map; + Rc diffuse_map; // Optional + Rc specular_map; // Optional + // Multiplied with the maps when present, used alone otherwise glm::vec3 diffuse_color{1.0f, 1.0f, 1.0f}; glm::vec3 specular_color{1.0f, 1.0f, 1.0f}; float shininess{32.0f}; + // Binds the shader and sets the u_material uniforms. Uses texture units + // 0 (diffuse) and 1 (specular) void bind() const; }; diff --git a/src/mesh.cpp b/src/mesh.cpp index 2874744..acb4fa2 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -2,6 +2,7 @@ #include #include "mesh.hpp" +#include "gldebug.hpp" #include "logger.hpp" namespace Fan @@ -11,9 +12,34 @@ Rc Mesh::create(const std::vector& vertices, const std::vector& indices/* = {}*/, const std::vector& submeshes/* = {}*/) { - GLuint VBO{}, VAO{}, EBO{}; bool has_indices = !indices.empty(); + // Validate inputs: out of range indices make the GPU read past the buffer + if (vertices.empty()) { + Log::error() << "Could not create mesh: no vertices"; + return nullptr; + } + + for (GLuint index : indices) { + if (index >= vertices.size()) { + Log::error() << "Could not create mesh: index " << index + << " out of range (" << vertices.size() << " vertices)"; + return nullptr; + } + } + + size_t element_count = has_indices ? indices.size() : vertices.size(); + for (const auto& submesh : submeshes) { + if (static_cast(submesh.index_offset) + submesh.index_count > element_count) { + Log::error() << "Could not create mesh: submesh range (offset " << submesh.index_offset + << ", count " << submesh.index_count << ") exceeds " << element_count; + return nullptr; + } + } + + gl_clear_errors(); + + GLuint VBO{}, VAO{}, EBO{}; glGenBuffers(1, &VBO); glGenVertexArrays(1, &VAO); glBindVertexArray(VAO); @@ -21,14 +47,14 @@ Rc Mesh::create(const std::vector& vertices, // Upload vertex data glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(vertices[0]), - &vertices[0], GL_DYNAMIC_DRAW); + vertices.data(), GL_DYNAMIC_DRAW); // Upload index data if (has_indices) { glGenBuffers(1, &EBO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(indices[0]), - &indices[0], GL_STATIC_DRAW); + indices.data(), GL_STATIC_DRAW); } // Define vertex data layout @@ -40,6 +66,19 @@ Rc Mesh::create(const std::vector& vertices, glEnableVertexAttribArray(1); glEnableVertexAttribArray(2); + // Unbind so later GL_ELEMENT_ARRAY_BUFFER binds can't modify this VAO + glBindVertexArray(0); + + // Buffer uploads can fail with GL_OUT_OF_MEMORY on large meshes + GLenum err = gl_pop_error(); + if (err != GL_NO_ERROR) { + Log::error() << "Could not upload mesh data: " << gl_error_string(err); + glDeleteBuffers(1, &EBO); + glDeleteBuffers(1, &VBO); + glDeleteVertexArrays(1, &VAO); + return nullptr; + } + // Submesh setup std::vector final_submeshes = submeshes; @@ -55,6 +94,56 @@ Rc Mesh::create(const std::vector& vertices, return make_rc(vertices, indices, final_submeshes, has_indices, VBO, VAO, EBO); } +Rc Mesh::create_cube() +{ + const std::vector vertices{ + // Back Face + {{ 0.5f, -0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 0.0f, 0.0f}}, + {{-0.5f, -0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 1.0f, 0.0f}}, + {{-0.5f, 0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 1.0f, 1.0f}}, + {{-0.5f, 0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 1.0f, 1.0f}}, + {{ 0.5f, 0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 0.0f, 1.0f}}, + {{ 0.5f, -0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 0.0f, 0.0f}}, + // Front Face + {{-0.5f, -0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 0.0f, 0.0f}}, + {{ 0.5f, -0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 1.0f, 0.0f}}, + {{ 0.5f, 0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 1.0f, 1.0f}}, + {{ 0.5f, 0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 1.0f, 1.0f}}, + {{-0.5f, 0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 0.0f, 1.0f}}, + {{-0.5f, -0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 0.0f, 0.0f}}, + // Left Face + {{-0.5f, -0.5f, -0.5f}, {-1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f}}, + {{-0.5f, -0.5f, 0.5f}, {-1.0f, 0.0f, 0.0f}, { 1.0f, 0.0f}}, + {{-0.5f, 0.5f, 0.5f}, {-1.0f, 0.0f, 0.0f}, { 1.0f, 1.0f}}, + {{-0.5f, 0.5f, 0.5f}, {-1.0f, 0.0f, 0.0f}, { 1.0f, 1.0f}}, + {{-0.5f, 0.5f, -0.5f}, {-1.0f, 0.0f, 0.0f}, { 0.0f, 1.0f}}, + {{-0.5f, -0.5f, -0.5f}, {-1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f}}, + // Right Face + {{ 0.5f, -0.5f, 0.5f}, { 1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f}}, + {{ 0.5f, -0.5f, -0.5f}, { 1.0f, 0.0f, 0.0f}, { 1.0f, 0.0f}}, + {{ 0.5f, 0.5f, -0.5f}, { 1.0f, 0.0f, 0.0f}, { 1.0f, 1.0f}}, + {{ 0.5f, 0.5f, -0.5f}, { 1.0f, 0.0f, 0.0f}, { 1.0f, 1.0f}}, + {{ 0.5f, 0.5f, 0.5f}, { 1.0f, 0.0f, 0.0f}, { 0.0f, 1.0f}}, + {{ 0.5f, -0.5f, 0.5f}, { 1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f}}, + // Bottom Face + {{-0.5f, -0.5f, -0.5f}, { 0.0f, -1.0f, 0.0f}, { 0.0f, 1.0f}}, + {{ 0.5f, -0.5f, -0.5f}, { 0.0f, -1.0f, 0.0f}, { 1.0f, 1.0f}}, + {{ 0.5f, -0.5f, 0.5f}, { 0.0f, -1.0f, 0.0f}, { 1.0f, 0.0f}}, + {{ 0.5f, -0.5f, 0.5f}, { 0.0f, -1.0f, 0.0f}, { 1.0f, 0.0f}}, + {{-0.5f, -0.5f, 0.5f}, { 0.0f, -1.0f, 0.0f}, { 0.0f, 0.0f}}, + {{-0.5f, -0.5f, -0.5f}, { 0.0f, -1.0f, 0.0f}, { 0.0f, 1.0f}}, + // Top Face + {{-0.5f, 0.5f, 0.5f}, { 0.0f, 1.0f, 0.0f}, { 0.0f, 0.0f}}, + {{ 0.5f, 0.5f, 0.5f}, { 0.0f, 1.0f, 0.0f}, { 1.0f, 0.0f}}, + {{ 0.5f, 0.5f, -0.5f}, { 0.0f, 1.0f, 0.0f}, { 1.0f, 1.0f}}, + {{ 0.5f, 0.5f, -0.5f}, { 0.0f, 1.0f, 0.0f}, { 1.0f, 1.0f}}, + {{-0.5f, 0.5f, -0.5f}, { 0.0f, 1.0f, 0.0f}, { 0.0f, 1.0f}}, + {{-0.5f, 0.5f, 0.5f}, { 0.0f, 1.0f, 0.0f}, { 0.0f, 0.0f}} + }; + + return create(vertices); +} + Mesh::~Mesh() { glDeleteBuffers(1, &m_EBO); diff --git a/src/mesh.hpp b/src/mesh.hpp index 440f0b5..807c0a8 100644 --- a/src/mesh.hpp +++ b/src/mesh.hpp @@ -14,6 +14,8 @@ namespace Fan class Mesh { public: + // A range drawn with one material. Offset and count are in indices, or in + // vertices if the mesh has no index buffer struct SubMesh { GLuint index_offset{}; @@ -21,9 +23,13 @@ public: Rc material{}; }; + // Without indices the vertices are drawn in order. Without submeshes a single + // one with no material covers the whole mesh static Rc create(const std::vector& vertices, const std::vector& indices = {}, const std::vector& submeshes = {}); + // Unit cube centered at the origin, with normals and texture coordinates + static Rc create_cube(); Mesh(const std::vector& vertices, const std::vector& indices, const std::vector& submeshes, @@ -45,6 +51,7 @@ public: const std::vector& get_submeshes() const; + // Geometry only: the caller binds the shader and material first void draw() const; void draw_submesh(GLuint index_offset, GLuint index_count) const; diff --git a/src/renderer.cpp b/src/renderer.cpp index 80e6ab4..0a5ba6c 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -1,9 +1,9 @@ #include -#include #include #include #include "renderer.hpp" +#include "resourcecache.hpp" #include "logger.hpp" namespace Fan @@ -12,7 +12,7 @@ namespace Fan Rc Renderer::s_camera_ubo = nullptr; Rc Renderer::s_light_ubo = nullptr; -void Renderer::init() +bool Renderer::init() { glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); @@ -22,6 +22,22 @@ void Renderer::init() using bp = Shader::BindingPoint; s_camera_ubo = UniformBuffer::create(sizeof(CameraData), bp::Camera); s_light_ubo = UniformBuffer::create(sizeof(LightEnvironmentData), bp::LightEnvironment); + + if (!s_camera_ubo || !s_light_ubo || !ResourceCache::init()) { + Log::error() << "Could not initialize renderer"; + shutdown(); + return false; + } + + return true; +} + +void Renderer::shutdown() +{ + // Release GPU resources while the GL context is still alive + ResourceCache::shutdown(); + s_camera_ubo.reset(); + s_light_ubo.reset(); } void Renderer::set_viewport(int x, int y, int width, int height) @@ -61,40 +77,27 @@ void Renderer::draw_scene(const Scene& scene, const Camera& camera, using RenderQueue = std::unordered_map>>; RenderQueue render_queue; + Material* fallback_material = ResourceCache::get_fallback_material().get(); + for (const auto& entity : scene.get_entities()) { if (!entity.mesh) { continue; } - // If Entity has an explicit material override - if (entity.material) { - if (!entity.material->shader) { continue; } + for (const auto& submesh : entity.mesh->get_submeshes()) { + // An explicit Entity material overrides the SubMesh's native one + Material* material_ptr = entity.material ? entity.material.get() + : submesh.material.get(); - Shader* shader_ptr = entity.material->shader.get(); - Material* material_ptr = entity.material.get(); - - for (const auto& submesh : entity.mesh->get_submeshes()) { - render_queue[shader_ptr][material_ptr].push_back({ - &entity.transform, - entity.mesh.get(), - submesh.index_offset, - submesh.index_count - }); + // Draw broken materials with the fallback instead of skipping them + if (!material_ptr || !material_ptr->shader) { + material_ptr = fallback_material; } - // Otherwise use SubMesh's native material - } else { - for (const auto& submesh : entity.mesh->get_submeshes()) { - if (!submesh.material || !submesh.material->shader) { continue; } - - Shader* shader_ptr = submesh.material->shader.get(); - Material* material_ptr = submesh.material.get(); - - render_queue[shader_ptr][material_ptr].push_back({ - &entity.transform, - entity.mesh.get(), - submesh.index_offset, - submesh.index_count - }); - } + render_queue[material_ptr->shader.get()][material_ptr].push_back({ + &entity.transform, + entity.mesh.get(), + submesh.index_offset, + submesh.index_count + }); } } @@ -106,7 +109,12 @@ void Renderer::draw_scene(const Scene& scene, const Camera& camera, material->bind(); for (const auto& cmd: draw_command) { - shader->set_mat4("u_model", cmd.transform->get_model_matrix()); + glm::mat4 model = cmd.transform->get_model_matrix(); + shader->set_mat4("u_model", model); + + // Inverse-transpose keeps normals perpendicular under non-uniform scale + shader->set_mat3("u_normal_matrix", glm::transpose(glm::inverse(glm::mat3(model)))); + cmd.mesh->draw_submesh(cmd.index_offset, cmd.index_count); } } @@ -157,12 +165,14 @@ void Renderer::draw_debug_wireframes(const Scene& scene, const Camera& camera, set_wireframe(false); } -void Renderer::update_global_data(const Scene& scene, const Camera& camera, int screen_width, int screen_height) +void Renderer::update_global_data(const Scene& scene, const Camera& camera, + int screen_width, int screen_height) { // Update camera UBO CameraData cam_data{}; cam_data.view = camera.get_view_matrix(); cam_data.projection = camera.get_projection_matrix(screen_width, screen_height); + cam_data.position = glm::vec4(camera.position, 1.0f); s_camera_ubo->update(&cam_data, sizeof(CameraData)); // Update light UBO @@ -177,7 +187,7 @@ void Renderer::update_global_data(const Scene& scene, const Camera& camera, int light_data.dir_light.specular = glm::vec4(dir_src.specular, 0.0f); // Map PointLights - if (light_data.active_point_lights > MAX_POINT_LIGHTS) { + if (env.point_lights.size() > MAX_POINT_LIGHTS) { Log::warn() << "Maximum number of point lights reached (" << MAX_POINT_LIGHTS << ")"; } light_data.active_point_lights = std::min(static_cast(env.point_lights.size()), MAX_POINT_LIGHTS); @@ -192,7 +202,7 @@ void Renderer::update_global_data(const Scene& scene, const Camera& camera, int } // Map SpotLights - if (light_data.active_spot_lights > MAX_SPOT_LIGHTS) { + if (env.spot_lights.size() > MAX_SPOT_LIGHTS) { Log::warn() << "Maximum number of spot lights reached (" << MAX_SPOT_LIGHTS << ")"; } light_data.active_spot_lights = std::min(static_cast(env.spot_lights.size()), MAX_SPOT_LIGHTS); diff --git a/src/renderer.hpp b/src/renderer.hpp index bf869a2..f73e157 100644 --- a/src/renderer.hpp +++ b/src/renderer.hpp @@ -11,15 +11,24 @@ namespace Fan class Renderer { public: - static void init(); + // init() needs a current GL context and also initializes the ResourceCache. + // shutdown() must run before the window (and its context) is destroyed + static bool init(); + static void shutdown(); static void set_viewport(int x, int y, int width, int height); static void set_clear_color(const glm::vec4& color); - static void set_wireframe(bool enabled); + static void set_wireframe(bool enabled); // Global, stays on until disabled static void clear(); + + // Draws into the currently bound framebuffer without clearing it. + // screen_width and screen_height only set the camera aspect ratio static void draw_scene(const Scene& scene, const Camera& camera, int screen_width, int screen_height); + + // debug_shader needs u_model, u_view_projection and u_color. default_mesh is + // drawn for entities without a mesh and for lights. Disables wireframe when done static void draw_debug_wireframes(const Scene& scene, const Camera& camera, int screen_width, int screen_height, const Rc& debug_shader, @@ -35,6 +44,7 @@ private: struct CameraData { glm::mat4 view; glm::mat4 projection; + glm::vec4 position; // w unused, std140 pads vec3 to 16 bytes anyway }; // Maps to Fan::DirectionalLight @@ -75,7 +85,6 @@ private: PointLightData point_lights[MAX_POINT_LIGHTS]; SpotLightData spot_lights[MAX_SPOT_LIGHTS]; }; - }; } // namespace Fan diff --git a/src/resourcecache.cpp b/src/resourcecache.cpp new file mode 100644 index 0000000..b4c5837 --- /dev/null +++ b/src/resourcecache.cpp @@ -0,0 +1,189 @@ +#include + +#include "resourcecache.hpp" +#include "logger.hpp" + +namespace Fan +{ + +std::unordered_map> ResourceCache::s_textures{}; +std::unordered_map> ResourceCache::s_shaders{}; +std::unordered_set ResourceCache::s_failed_textures{}; + +Rc ResourceCache::s_fallback_texture = nullptr; +Rc ResourceCache::s_fallback_shader = nullptr; +Rc ResourceCache::s_fallback_material = nullptr; +Rc ResourceCache::s_fallback_mesh = nullptr; + +// Unlit, shows the material's diffuse map (the checkerboard on the fallback material) +static constexpr const char* FALLBACK_VERTEX_SOURCE = R"(#version 330 core + +layout (location = 0) in vec3 a_position; +layout (location = 2) in vec2 a_texture_coords; + +out vec2 texture_coords; + +layout (std140) uniform CameraBlock { + mat4 view; + mat4 projection; + vec4 camera_position; // w unused +}; + +uniform mat4 u_model; + +void main() +{ + gl_Position = projection * view * u_model * vec4(a_position, 1.0); + texture_coords = a_texture_coords; +} +)"; + +static constexpr const char* FALLBACK_FRAGMENT_SOURCE = R"(#version 330 core + +struct Material { + sampler2D diffuse; +}; + +uniform Material u_material; + +in vec2 texture_coords; + +out vec4 frag_color; + +void main() +{ + frag_color = vec4(texture(u_material.diffuse, texture_coords).rgb, 1.0); +} +)"; + +static Rc create_checker_texture() +{ + constexpr int SIZE = 64; + constexpr int CELL_SIZE = 8; + + std::vector pixels(SIZE * SIZE * 4); + for (int y = 0; y < SIZE; ++y) { + for (int x = 0; x < SIZE; ++x) { + bool magenta = ((x / CELL_SIZE) + (y / CELL_SIZE)) % 2 == 0; + unsigned char* pixel = &pixels[(y * SIZE + x) * 4]; + + pixel[0] = magenta ? 255 : 0; + pixel[1] = 0; + pixel[2] = magenta ? 255 : 0; + pixel[3] = 255; + } + } + + return Texture::create_from_pixels(SIZE, SIZE, pixels.data(), "Fallback checkerboard"); +} + +bool ResourceCache::init() +{ + s_fallback_texture = create_checker_texture(); + s_fallback_mesh = Mesh::create_cube(); + + // Only the camera block is needed, binding the standard ones would log + // an error for the missing LightEnvironmentBlock + s_fallback_shader = Shader::create_from_source(FALLBACK_VERTEX_SOURCE, + FALLBACK_FRAGMENT_SOURCE, + "Fallback shader", false); + if (s_fallback_shader) { + s_fallback_shader->bind_uniform_block("CameraBlock", Shader::BindingPoint::Camera); + } + + // These are built from memory, so failing here means OpenGL itself is broken + if (!s_fallback_texture || !s_fallback_shader || !s_fallback_mesh) { + Log::error() << "Could not create fallback resources"; + shutdown(); + return false; + } + + s_fallback_material = make_rc(); + s_fallback_material->shader = s_fallback_shader; + s_fallback_material->diffuse_map = s_fallback_texture; + + return true; +} + +void ResourceCache::shutdown() +{ + // Release GPU resources while the GL context is still alive + s_textures.clear(); + s_shaders.clear(); + s_failed_textures.clear(); + + s_fallback_material.reset(); + s_fallback_texture.reset(); + s_fallback_shader.reset(); + s_fallback_mesh.reset(); +} + +Rc ResourceCache::load_texture(const std::string& path, bool use_fallback/* = true*/) +{ + if (s_failed_textures.count(path) > 0) { + return use_fallback ? s_fallback_texture : nullptr; + } + + Weak& entry = s_textures[path]; + if (auto cached = entry.lock()) { + return cached; + } + + Rc texture = Texture::create(path); + if (!texture) { + s_failed_textures.insert(path); + + if (!use_fallback) { + return nullptr; + } + + Log::warn() << "Using fallback texture for " << path; + return s_fallback_texture; + } + + entry = texture; + return texture; +} + +Rc ResourceCache::load_shader(const std::string& vertex_path, + const std::string& fragment_path, + bool bind_standard_blocks/* = true*/) +{ + std::string key = vertex_path + "|" + fragment_path + (bind_standard_blocks ? "|std" : ""); + + Weak& entry = s_shaders[key]; + if (auto cached = entry.lock()) { + return cached; + } + + Rc shader = Shader::create(vertex_path, fragment_path, bind_standard_blocks); + if (!shader) { + Log::warn() << "Using fallback shader for (" << vertex_path << ", " << fragment_path << ")"; + shader = s_fallback_shader; + } + + entry = shader; + return shader; +} + +const Rc& ResourceCache::get_fallback_texture() +{ + return s_fallback_texture; +} + +const Rc& ResourceCache::get_fallback_shader() +{ + return s_fallback_shader; +} + +const Rc& ResourceCache::get_fallback_material() +{ + return s_fallback_material; +} + +const Rc& ResourceCache::get_fallback_mesh() +{ + return s_fallback_mesh; +} + +} // namespace Fan diff --git a/src/resourcecache.hpp b/src/resourcecache.hpp new file mode 100644 index 0000000..ac32c97 --- /dev/null +++ b/src/resourcecache.hpp @@ -0,0 +1,56 @@ +#ifndef FAN_RESOURCECACHE_HPP_ +#define FAN_RESOURCECACHE_HPP_ + +#include +#include +#include + +#include "material.hpp" +#include "mesh.hpp" +#include "shader.hpp" +#include "texture.hpp" +#include "utils.hpp" + +namespace Fan +{ + +// Shares loaded resources and substitutes fallbacks for the ones that fail to +// load, so a broken asset shows up as a magenta checkerboard instead of a +// crash. Initialized and shut down by the Renderer. +class ResourceCache +{ +public: + static bool init(); + static void shutdown(); + + // Never return null after init(). A failed load is logged once and the + // fallback is returned for that path from then on. + // use_fallback: false for optional textures (e.g. specular maps), which + // return null on failure so the material can do without them + static Rc load_texture(const std::string& path, bool use_fallback = true); + static Rc load_shader(const std::string& vertex_path, + const std::string& fragment_path, + bool bind_standard_blocks = true); + + static const Rc& get_fallback_texture(); + static const Rc& get_fallback_shader(); + static const Rc& get_fallback_material(); + static const Rc& get_fallback_mesh(); + +private: + // Weak references: the cache doesn't keep unused resources alive + static std::unordered_map> s_textures; + static std::unordered_map> s_shaders; + + // Paths that failed to load, so they aren't retried (and re-logged) + static std::unordered_set s_failed_textures; + + static Rc s_fallback_texture; + static Rc s_fallback_shader; + static Rc s_fallback_material; + static Rc s_fallback_mesh; +}; + +} // namespace Fan + +#endif // FAN_RESOURCECACHE_HPP_ diff --git a/src/scene.hpp b/src/scene.hpp index 92abd6e..7315993 100644 --- a/src/scene.hpp +++ b/src/scene.hpp @@ -14,6 +14,8 @@ class Scene public: LightEnvironment light_environment{}; + // Stores a copy: later changes to the passed Entity don't affect the scene. + // Meshes and materials are shared, not copied void add_entity(const Entity& entity); const std::vector& get_entities() const; diff --git a/src/shader.cpp b/src/shader.cpp index df59687..830896d 100644 --- a/src/shader.cpp +++ b/src/shader.cpp @@ -3,6 +3,7 @@ #include "shader.hpp" #include "utils.hpp" +#include "gldebug.hpp" #include "logger.hpp" namespace Fan @@ -12,18 +13,47 @@ Rc Shader::create(const std::string& vertex_shader_path, const std::string& fragment_shader_path, bool bind_standard_blocks/* = true*/) { - auto vertex_shader = compile_shader(vertex_shader_path, GL_VERTEX_SHADER); + auto vertex_source = read_entire_file(vertex_shader_path); + if (!vertex_source) { + Log::error() << "Could not read shader " << vertex_shader_path; + return nullptr; + } + + auto fragment_source = read_entire_file(fragment_shader_path); + if (!fragment_source) { + Log::error() << "Could not read shader " << fragment_shader_path; + return nullptr; + } + + return create_from_source(*vertex_source, *fragment_source, + vertex_shader_path + ", " + fragment_shader_path, + bind_standard_blocks); +} + +Rc Shader::create_from_source(const std::string& vertex_source, + const std::string& fragment_source, + const std::string& name, + bool bind_standard_blocks/* = true*/) +{ + auto vertex_shader = compile_shader(vertex_source, GL_VERTEX_SHADER, name); if (!vertex_shader) { return nullptr; } - auto fragment_shader = compile_shader(fragment_shader_path, GL_FRAGMENT_SHADER); + auto fragment_shader = compile_shader(fragment_source, GL_FRAGMENT_SHADER, name); if (!fragment_shader) { glDeleteShader(*vertex_shader); return nullptr; } GLuint shader_program = glCreateProgram(); + if (shader_program == 0) { + Log::error() << "glCreateProgram failed for " << name; + glDeleteShader(*vertex_shader); + glDeleteShader(*fragment_shader); + return nullptr; + } + glAttachShader(shader_program, *vertex_shader); glAttachShader(shader_program, *fragment_shader); glLinkProgram(shader_program); @@ -40,22 +70,23 @@ Rc Shader::create(const std::string& vertex_shader_path, if (log_length > 0) { std::string log(log_length, ' '); glGetProgramInfoLog(shader_program, log_length, nullptr, &log[0]); - Log::error() << "Could not link shader program: " << log; + Log::error() << "Could not link shader program (" << name << "): " << log; } glDeleteProgram(shader_program); return nullptr; } - Rc shader = make_rc(shader_program); + set_gl_label(GL_PROGRAM, shader_program, name); + + Rc shader = make_rc(shader_program, name); if (bind_standard_blocks) { shader->bind_uniform_block("CameraBlock", BindingPoint::Camera); shader->bind_uniform_block("LightEnvironmentBlock", BindingPoint::LightEnvironment); } - Log::info() << "Created shader (" << vertex_shader_path << ", " - << fragment_shader_path << ")"; + Log::info() << "Created shader (" << name << ")"; return shader; } @@ -66,6 +97,8 @@ Shader::~Shader() Shader::Shader(Shader&& other) noexcept : m_id(std::exchange(other.m_id, 0)) + , m_name(std::move(other.m_name)) + , m_uniform_locations(std::move(other.m_uniform_locations)) { } @@ -74,6 +107,8 @@ Shader& Shader::operator=(Shader&& other) noexcept if (this == &other) { return *this; } std::swap(m_id, other.m_id); + std::swap(m_name, other.m_name); + std::swap(m_uniform_locations, other.m_uniform_locations); return *this; } @@ -83,52 +118,75 @@ GLuint Shader::get_id() const return m_id; } -GLint Shader::get_location(const std::string &name) +const std::string& Shader::get_name() const { - GLint location = glGetUniformLocation(m_id, name.c_str()); - if (location == -1) { - Log::warn() << "Uniform " << name << " not found"; + return m_name; +} + +GLint Shader::get_location(std::string_view name) +{ + auto it = m_uniform_locations.find(name); + if (it != m_uniform_locations.end()) { + return it->second; } + + // First lookup of this name: ask the driver and remember the result, + // including misses so the warning is only printed once + std::string name_str{name}; + GLint location = glGetUniformLocation(m_id, name_str.c_str()); + if (location == -1) { + Log::warn() << "Uniform " << name_str << " not found in shader (" << m_name << ")"; + } + + m_uniform_locations.emplace(std::move(name_str), location); return location; } -void Shader::set_int(const std::string& name, GLint value) +void Shader::set_int(std::string_view name, GLint value) { glUniform1i(get_location(name), value); } -void Shader::set_float(const std::string& name, float value) +void Shader::set_float(std::string_view name, float value) { glUniform1f(get_location(name), value); } -void Shader::set_vec3(const std::string& name, float x, float y, float z) +void Shader::set_vec3(std::string_view name, float x, float y, float z) { glUniform3f(get_location(name), x, y, z); } -void Shader::set_vec3(const std::string& name, const glm::vec3& value) +void Shader::set_vec3(std::string_view name, const glm::vec3& value) { glUniform3f(get_location(name), value.x, value.y, value.z); } -void Shader::set_mat4(const std::string& name, const glm::mat4& value) +void Shader::set_mat3(std::string_view name, const glm::mat3& value) +{ + GLint location = get_location(name); + glUniformMatrix3fv(location, 1, GL_FALSE, glm::value_ptr(value)); +} + +void Shader::set_mat4(std::string_view name, const glm::mat4& value) { GLint location = get_location(name); glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(value)); } -Opt Shader::compile_shader(const std::string& source_path, - GLenum shader_type) +Opt Shader::compile_shader(const std::string& source, + GLenum shader_type, + const std::string& name) { - auto shader_source = read_entire_file(source_path); - if (!shader_source) { - Log::error() << "Could not read shader " << source_path; + const char* type_name = shader_type == GL_VERTEX_SHADER ? "vertex" : "fragment"; + + GLuint shader = glCreateShader(shader_type); + if (shader == 0) { + Log::error() << "glCreateShader failed for " << type_name << " shader (" << name << ")"; return std::nullopt; } - GLuint shader = glCreateShader(shader_type); - const char *shader_source_str = shader_source->c_str(); + const char *shader_source_str = source.c_str(); glShaderSource(shader, 1, &shader_source_str, nullptr); glCompileShader(shader); @@ -141,7 +199,7 @@ Opt Shader::compile_shader(const std::string& source_path, if (log_length > 0) { std::string log(log_length, ' '); glGetShaderInfoLog(shader, log_length, nullptr, &log[0]); - Log::error() << "Could not compile shader: " << log; + Log::error() << "Could not compile " << type_name << " shader (" << name << "): " << log; } glDeleteShader(shader); diff --git a/src/shader.hpp b/src/shader.hpp index a66e9d5..17a5c04 100644 --- a/src/shader.hpp +++ b/src/shader.hpp @@ -1,7 +1,10 @@ #ifndef FAN_SHADER_HPP_ #define FAN_SHADER_HPP_ +#include +#include #include +#include #include #include @@ -13,16 +16,23 @@ namespace Fan class Shader { public: + // Uniform buffer binding points shared by all shaders struct BindingPoint { static constexpr GLuint Camera = 0; static constexpr GLuint LightEnvironment = 1; }; + // bind_standard_blocks: bind CameraBlock and LightEnvironmentBlock to their + // BindingPoint. Logs an error for each block the shader doesn't declare static Rc create(const std::string& vertex_path, const std::string& fragment_path, bool bind_standard_blocks = true); - Shader(GLuint id) : m_id{id} {} + static Rc create_from_source(const std::string& vertex_source, + const std::string& fragment_source, + const std::string& name, + bool bind_standard_blocks = true); + Shader(GLuint id, const std::string& name) : m_id{id}, m_name{name} {} ~Shader(); Shader(const Shader&) = delete; @@ -31,23 +41,31 @@ public: Shader& operator=(Shader&& other) noexcept; GLuint get_id() const; - GLint get_location(const std::string &name); + const std::string& get_name() const; + GLint get_location(std::string_view name); // -1 if not found (warns once per name) // Uniform helpers - void set_int(const std::string& name, GLint value); - void set_float(const std::string& name, float value); - void set_vec3(const std::string& name, float x, float y, float z); - void set_vec3(const std::string& name, const glm::vec3& value); - void set_mat4(const std::string& name, const glm::mat4& value); + void set_int(std::string_view name, GLint value); + void set_float(std::string_view name, float value); + void set_vec3(std::string_view name, float x, float y, float z); + void set_vec3(std::string_view name, const glm::vec3& value); + void set_mat3(std::string_view name, const glm::mat3& value); + void set_mat4(std::string_view name, const glm::mat4& value); void bind() const; void bind_uniform_block(const std::string& name, GLuint binding_point) const; private: GLuint m_id{}; + std::string m_name{}; - static Opt compile_shader(const std::string& source_path, - GLenum shader_type); + // Uniform name -> location (-1 if not found). std::less<> allows lookups + // with a string_view, so cache hits don't allocate a std::string + std::map> m_uniform_locations{}; + + static Opt compile_shader(const std::string& source, + GLenum shader_type, + const std::string& name); }; } // namespace Fan diff --git a/src/shaders/default.frag b/src/shaders/default.frag index 9c58d0b..d887da8 100644 --- a/src/shaders/default.frag +++ b/src/shaders/default.frag @@ -42,6 +42,7 @@ struct SpotLight { layout (std140) uniform CameraBlock { mat4 view; mat4 projection; + vec4 camera_position; // w unused }; #define MAX_POINT_LIGHTS 32 @@ -63,100 +64,111 @@ in vec2 texture_coords; out vec4 frag_color; -vec3 get_diffuse() { - if (u_material.use_diffuse_map == 1) { - return vec3(texture(u_material.diffuse, texture_coords)) * u_material.diffuse_color; - } - return u_material.diffuse_color; -} +// Light reaching the surface, grouped by the material color it gets multiplied +// with. Applying the material once at the end saves work per light +struct Lighting { + vec3 diffuse; // ambient + diffuse light, multiplied by the albedo + vec3 specular; // multiplied by the specular color +}; -vec3 get_specular() { - if (u_material.use_specular_map == 1) { - return vec3(texture(u_material.specular, texture_coords)) * u_material.specular_color; - } - return u_material.specular_color; -} - -vec3 calculate_directional_light(DirLight light, vec3 normal, vec3 view_dir); -vec3 calculate_point_light(PointLight light, vec3 normal, vec3 frag_position, vec3 view_dir); -vec3 calculate_spot_light(SpotLight light, vec3 normal, vec3 frag_position, vec3 view_dir); +vec2 phong(vec3 normal, vec3 light_dir, vec3 view_dir); +void add_directional_light(DirLight light, vec3 normal, vec3 view_dir, inout Lighting lighting); +void add_point_light(PointLight light, vec3 normal, vec3 view_dir, inout Lighting lighting); +void add_spot_light(SpotLight light, vec3 normal, vec3 view_dir, inout Lighting lighting); void main() { - vec4 texture_color = texture(u_material.diffuse, texture_coords); - if (texture_color.a < 1e-2) { - discard; + // Sample each texture once, the light functions never touch the material + vec4 diffuse_sample = vec4(1.0); + if (u_material.use_diffuse_map == 1) { + diffuse_sample = texture(u_material.diffuse, texture_coords); + + // Discard if diffuse map has a low alpha value + if (diffuse_sample.a < 0.01) { + discard; + } } - vec3 norm = normalize(normal); + vec3 specular_sample = vec3(1.0); + if (u_material.use_specular_map == 1) { + specular_sample = texture(u_material.specular, texture_coords).rgb; + } - vec3 view_pos = vec3(inverse(view)[3]); - vec3 view_dir = normalize(view_pos - frag_position); + vec3 albedo = diffuse_sample.rgb * u_material.diffuse_color; + vec3 specular_color = specular_sample * u_material.specular_color; + + vec3 norm = normalize(normal); + vec3 view_dir = normalize(camera_position.xyz - frag_position); + + Lighting lighting = Lighting(vec3(0.0), vec3(0.0)); // DirectionalLight - vec3 result = calculate_directional_light(dir_light, norm, view_dir); + add_directional_light(dir_light, norm, view_dir, lighting); // PointLights for (int i = 0; i < active_point_lights; ++i) { - result += calculate_point_light(point_lights[i], norm, frag_position, view_dir); + add_point_light(point_lights[i], norm, view_dir, lighting); } // SpotLights for (int i = 0; i < active_spot_lights; ++i) { - result += calculate_spot_light(spot_lights[i], norm, frag_position, view_dir); + add_spot_light(spot_lights[i], norm, view_dir, lighting); } - frag_color = vec4(result, 1.0); + frag_color = vec4(lighting.diffuse * albedo + lighting.specular * specular_color, 1.0); } -vec3 calculate_directional_light(DirLight light, vec3 normal, vec3 view_dir) +// Diffuse (x) and specular (y) factors for a normalized surface-to-light direction +vec2 phong(vec3 normal, vec3 light_dir, vec3 view_dir) +{ + float diff = max(dot(normal, light_dir), 0.0); + vec3 reflect_dir = reflect(-light_dir, normal); + float spec = pow(max(dot(view_dir, reflect_dir), 0.0), u_material.shininess); + + return vec2(diff, spec); +} + +void add_directional_light(DirLight light, vec3 normal, vec3 view_dir, inout Lighting lighting) { vec3 light_dir = normalize(-light.direction.xyz); - float diff = max(dot(normal, light_dir), 0.0); - vec3 reflect_dir = reflect(-light_dir, normal); - float spec = pow(max(dot(view_dir, reflect_dir), 0.0), u_material.shininess); + vec2 factors = phong(normal, light_dir, view_dir); - vec3 ambient = light.ambient.xyz * get_diffuse(); - vec3 diffuse = light.diffuse.xyz * diff * get_diffuse(); - vec3 specular = light.specular.xyz * spec * get_specular(); - - return ambient + diffuse + specular; + lighting.diffuse += light.ambient.xyz + light.diffuse.xyz * factors.x; + lighting.specular += light.specular.xyz * factors.y; } -vec3 calculate_point_light(PointLight light, vec3 normal, vec3 frag_position, vec3 view_dir) +void add_point_light(PointLight light, vec3 normal, vec3 view_dir, inout Lighting lighting) { - vec3 light_dir = normalize(light.position.xyz - frag_position); - float diff = max(dot(normal, light_dir), 0.0); - vec3 reflect_dir = reflect(-light_dir, normal); - float spec = pow(max(dot(view_dir, reflect_dir), 0.0), u_material.shininess); + // One length() gives both the distance and the normalized direction + vec3 to_light = light.position.xyz - frag_position; + float dist = length(to_light); + vec3 light_dir = to_light / dist; - float dist = length(light.position.xyz - frag_position); float attenuation = 1.0 / (light.attenuation.x + light.attenuation.y * dist + light.attenuation.z * (dist * dist)); + vec2 factors = phong(normal, light_dir, view_dir); - vec3 ambient = light.ambient.xyz * get_diffuse(); - vec3 diffuse = light.diffuse.xyz * diff * get_diffuse(); - vec3 specular = light.specular.xyz * spec * get_specular(); - - return (ambient + diffuse + specular) * attenuation; + lighting.diffuse += (light.ambient.xyz + light.diffuse.xyz * factors.x) * attenuation; + lighting.specular += light.specular.xyz * factors.y * attenuation; } -vec3 calculate_spot_light(SpotLight light, vec3 normal, vec3 frag_position, vec3 view_dir) +void add_spot_light(SpotLight light, vec3 normal, vec3 view_dir, inout Lighting lighting) { - vec3 light_dir = normalize(light.position.xyz - frag_position); - float diff = max(dot(normal, light_dir), 0.0); - vec3 reflect_dir = reflect(-light_dir, normal); - float spec = pow(max(dot(view_dir, reflect_dir), 0.0), u_material.shininess); - - float dist = length(light.position.xyz - frag_position); - float attenuation = 1.0 / (light.attenuation.x + light.attenuation.y * dist + light.attenuation.z * (dist * dist)); + vec3 to_light = light.position.xyz - frag_position; + float dist = length(to_light); + vec3 light_dir = to_light / dist; + // Outside the outer cone the intensity is 0, skip the rest of the work float theta = dot(light_dir, normalize(-light.direction.xyz)); + if (theta <= light.cut_offs.y) { + return; + } + float epsilon = light.cut_offs.x - light.cut_offs.y; float intensity = clamp((theta - light.cut_offs.y) / epsilon, 0.0, 1.0); - vec3 ambient = light.ambient.xyz * get_diffuse(); - vec3 diffuse = light.diffuse.xyz * diff * get_diffuse(); - vec3 specular = light.specular.xyz * spec * get_specular(); + float attenuation = 1.0 / (light.attenuation.x + light.attenuation.y * dist + light.attenuation.z * (dist * dist)); + vec2 factors = phong(normal, light_dir, view_dir); - return (ambient + diffuse + specular) * attenuation * intensity; + lighting.diffuse += (light.ambient.xyz + light.diffuse.xyz * factors.x) * attenuation * intensity; + lighting.specular += light.specular.xyz * factors.y * attenuation * intensity; } diff --git a/src/shaders/default.vert b/src/shaders/default.vert index 2982ec0..b11d5c9 100644 --- a/src/shaders/default.vert +++ b/src/shaders/default.vert @@ -11,9 +11,11 @@ out vec2 texture_coords; layout (std140) uniform CameraBlock { mat4 view; mat4 projection; + vec4 camera_position; // w unused }; uniform mat4 u_model; +uniform mat3 u_normal_matrix; // Inverse-transpose of u_model, computed on the CPU void main() { @@ -21,8 +23,7 @@ void main() frag_position = vec3(u_model * vec4(a_position, 1.0)); - mat3 normal_matrix = mat3(transpose(inverse(u_model))); // TODO: Calculate on CPU for performance - normal = normal_matrix * a_normal; + normal = u_normal_matrix * a_normal; texture_coords = a_texture_coords; } diff --git a/src/texture.cpp b/src/texture.cpp index f4d481f..f11d9c5 100644 --- a/src/texture.cpp +++ b/src/texture.cpp @@ -1,6 +1,7 @@ #include #include "texture.hpp" +#include "gldebug.hpp" #include "logger.hpp" namespace Fan @@ -17,15 +18,30 @@ Rc Texture::create(const std::string& image_path) return nullptr; } - GLuint texture; - glGenTextures(1, &texture); - if (glGetError() != GL_NO_ERROR) { - Log::error() << "glGenTextures failed"; - stbi_image_free(image_data); + auto texture = create_from_pixels(width, height, image_data, image_path); + stbi_image_free(image_data); + + return texture; +} + +Rc Texture::create_from_pixels(GLsizei width, GLsizei height, + const unsigned char* pixels, + const std::string& name) +{ + GLint max_size = 0; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_size); + if (width <= 0 || height <= 0 || width > max_size || height > max_size) { + Log::error() << "Texture " << name << " (" << width << "x" << height + << ") is empty or exceeds the maximum size of " << max_size; return nullptr; } + gl_clear_errors(); + + GLuint texture; + glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); + set_gl_label(GL_TEXTURE, texture, name); // Texture wrapping parameters glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); @@ -35,10 +51,16 @@ Rc Texture::create(const std::string& image_path) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image_data); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); glGenerateMipmap(GL_TEXTURE_2D); - stbi_image_free(image_data); + // Upload can fail with GL_OUT_OF_MEMORY on large images + GLenum err = gl_pop_error(); + if (err != GL_NO_ERROR) { + Log::error() << "Could not upload texture " << name << ": " << gl_error_string(err); + glDeleteTextures(1, &texture); + return nullptr; + } return make_rc(texture); } diff --git a/src/texture.hpp b/src/texture.hpp index 2825d19..63d5e94 100644 --- a/src/texture.hpp +++ b/src/texture.hpp @@ -12,7 +12,13 @@ namespace Fan class Texture { public: + // Uploaded as RGBA8 with mipmaps and repeat wrapping. The image is flipped + // vertically so UV (0, 0) is its bottom-left corner static Rc create(const std::string& image_path); + // Pixels are tightly packed RGBA8, first row at the bottom (OpenGL convention) + static Rc create_from_pixels(GLsizei width, GLsizei height, + const unsigned char* pixels, + const std::string& name); Texture(GLuint id) : m_id{id} {} ~Texture(); @@ -23,6 +29,7 @@ public: GLuint get_id() const; + // slot is the texture unit, which is left as the active unit void bind(GLuint slot = 0) const; static void unbind(GLuint slot = 0); diff --git a/src/timer.cpp b/src/timer.cpp new file mode 100644 index 0000000..a86176d --- /dev/null +++ b/src/timer.cpp @@ -0,0 +1,130 @@ +#include + +#include "timer.hpp" +#include "gldebug.hpp" +#include "logger.hpp" + +namespace Fan +{ + +void CpuTimer::begin() +{ + m_start = std::chrono::steady_clock::now(); +} + +void CpuTimer::end() +{ + auto elapsed = std::chrono::steady_clock::now() - m_start; + m_elapsed_ms = std::chrono::duration(elapsed).count(); +} + +float CpuTimer::get_elapsed_ms() const +{ + return m_elapsed_ms; +} + +Rc GpuTimer::create() +{ + // Drivers report 0 counter bits when timer queries aren't usable + GLint counter_bits = 0; + glGetQueryiv(GL_TIME_ELAPSED, GL_QUERY_COUNTER_BITS, &counter_bits); + if (counter_bits == 0) { + Log::error() << "Could not create GPU timer: timer queries not supported"; + return nullptr; + } + + gl_clear_errors(); + + std::array queries{}; + glGenQueries(static_cast(QUERY_COUNT), queries.data()); + + GLenum err = gl_pop_error(); + if (err != GL_NO_ERROR) { + Log::error() << "Could not create GPU timer: " << gl_error_string(err); + glDeleteQueries(static_cast(QUERY_COUNT), queries.data()); + return nullptr; + } + + return make_rc(queries); +} + +GpuTimer::~GpuTimer() +{ + glDeleteQueries(static_cast(QUERY_COUNT), m_queries.data()); +} + +GpuTimer::GpuTimer(GpuTimer&& other) noexcept + : m_queries(std::exchange(other.m_queries, {})) + , m_pending(std::exchange(other.m_pending, {})) + , m_current(std::exchange(other.m_current, 0)) + , m_elapsed_ms(std::exchange(other.m_elapsed_ms, 0.0f)) + , m_active(std::exchange(other.m_active, false)) +{ +} + +GpuTimer& GpuTimer::operator=(GpuTimer&& other) noexcept +{ + if (this == &other) { return *this; } + + std::swap(m_queries, other.m_queries); + std::swap(m_pending, other.m_pending); + std::swap(m_current, other.m_current); + std::swap(m_elapsed_ms, other.m_elapsed_ms); + std::swap(m_active, other.m_active); + + return *this; +} + +void GpuTimer::begin() +{ + if (m_active) { + Log::warn() << "GpuTimer::begin() called while already active"; + return; + } + + // Reusing a slot drops its result if the GPU still hasn't delivered it + m_pending[m_current] = false; + glBeginQuery(GL_TIME_ELAPSED, m_queries[m_current]); + m_active = true; +} + +void GpuTimer::end() +{ + if (!m_active) { + Log::warn() << "GpuTimer::end() called without begin()"; + return; + } + + glEndQuery(GL_TIME_ELAPSED); + m_active = false; + m_pending[m_current] = true; + m_current = (m_current + 1) % QUERY_COUNT; + + collect_results(); +} + +float GpuTimer::get_elapsed_ms() const +{ + return m_elapsed_ms; +} + +void GpuTimer::collect_results() +{ + // m_current now points to the oldest query. Results become available in + // submission order, so stop at the first one that isn't ready yet + for (size_t i = 0; i < QUERY_COUNT; ++i) { + size_t index = (m_current + i) % QUERY_COUNT; + if (!m_pending[index]) { continue; } + + GLint available = GL_FALSE; + glGetQueryObjectiv(m_queries[index], GL_QUERY_RESULT_AVAILABLE, &available); + if (!available) { break; } + + GLuint64 elapsed_ns = 0; + glGetQueryObjectui64v(m_queries[index], GL_QUERY_RESULT, &elapsed_ns); + m_elapsed_ms = static_cast(elapsed_ns) / 1'000'000.0f; + m_pending[index] = false; + } +} + +} // namespace Fan diff --git a/src/timer.hpp b/src/timer.hpp new file mode 100644 index 0000000..039f95b --- /dev/null +++ b/src/timer.hpp @@ -0,0 +1,64 @@ +#ifndef FAN_TIMER_HPP_ +#define FAN_TIMER_HPP_ + +#include +#include +#include +#include + +#include "utils.hpp" + +namespace Fan +{ + +// Measures CPU wall-clock time between begin() and end() +class CpuTimer +{ +public: + void begin(); + void end(); + + float get_elapsed_ms() const; + +private: + std::chrono::steady_clock::time_point m_start{}; + float m_elapsed_ms{}; +}; + +// Measures how long the GPU spends executing the commands between begin() and +// end(). Results are read back a few frames later so the CPU never waits on the +// GPU. GL_TIME_ELAPSED queries can't be nested: only one GpuTimer may be active +class GpuTimer +{ +public: + // Frames the GPU may lag behind before a measurement is dropped + static constexpr size_t QUERY_COUNT = 4; + + static Rc create(); + GpuTimer(const std::array& queries) : m_queries{queries} {} + + ~GpuTimer(); + GpuTimer(const GpuTimer&) = delete; + GpuTimer& operator=(const GpuTimer&) = delete; + GpuTimer(GpuTimer&& other) noexcept; + GpuTimer& operator=(GpuTimer&& other) noexcept; + + void begin(); + void end(); + + // Latest available measurement, a few frames old + float get_elapsed_ms() const; + +private: + std::array m_queries{}; + std::array m_pending{}; + size_t m_current{}; + float m_elapsed_ms{}; + bool m_active{}; + + void collect_results(); +}; + +} // namespace Fan + +#endif // FAN_TIMER_HPP_ diff --git a/src/transform.hpp b/src/transform.hpp index a8600e2..97278b0 100644 --- a/src/transform.hpp +++ b/src/transform.hpp @@ -19,16 +19,16 @@ struct Transform explicit Transform(const glm::vec3& position) : position(position) {} - // Rotate relative to current orientation + // Rotate around a world-space axis, on top of the current rotation void rotate(float angle_rad, const glm::vec3& axis); - // Get the model matrix of the transform + // Get the model matrix of the transform (scale, then rotate, then translate) glm::mat4 get_model_matrix() const; // Get Euler angles in radians glm::vec3 get_euler_angles() const; - // Get direction vectors + // Get direction vectors (forward is -Z, OpenGL convention) glm::vec3 get_forward() const; glm::vec3 get_right() const; glm::vec3 get_up() const; diff --git a/src/uniformbuffer.cpp b/src/uniformbuffer.cpp index bccd227..94d455b 100644 --- a/src/uniformbuffer.cpp +++ b/src/uniformbuffer.cpp @@ -1,6 +1,7 @@ #include #include "uniformbuffer.hpp" +#include "gldebug.hpp" #include "logger.hpp" namespace Fan @@ -8,6 +9,23 @@ namespace Fan Rc UniformBuffer::create(size_t size, GLuint binding_point) { + GLint max_size = 0, max_bindings = 0; + glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &max_size); + glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &max_bindings); + + if (size == 0 || size > static_cast(max_size)) { + Log::error() << "Could not create uniformbuffer: size " << size + << " outside of (0, " << max_size << "]"; + return nullptr; + } + if (binding_point >= static_cast(max_bindings)) { + Log::error() << "Could not create uniformbuffer: binding point " << binding_point + << " exceeds maximum of " << max_bindings - 1; + return nullptr; + } + + gl_clear_errors(); + GLuint id; glGenBuffers(1, &id); @@ -17,9 +35,9 @@ Rc UniformBuffer::create(size_t size, GLuint binding_point) glBindBufferBase(GL_UNIFORM_BUFFER, binding_point, id); glBindBuffer(GL_UNIFORM_BUFFER, 0); - GLenum err = glGetError(); + GLenum err = gl_pop_error(); if (err != GL_NO_ERROR) { - Log::error() << "Could not create uniformbuffer"; + Log::error() << "Could not create uniformbuffer: " << gl_error_string(err); glDeleteBuffers(1, &id); return nullptr; } diff --git a/src/uniformbuffer.hpp b/src/uniformbuffer.hpp index 848a799..153fffd 100644 --- a/src/uniformbuffer.hpp +++ b/src/uniformbuffer.hpp @@ -12,6 +12,8 @@ namespace Fan class UniformBuffer { public: + // The buffer stays bound to binding_point. Shader blocks reach it by being + // bound to the same point (see Shader::bind_uniform_block) static Rc create(size_t size, GLuint binding_point); UniformBuffer(GLuint id, size_t size, GLuint binding_point) : m_id(id) @@ -28,7 +30,8 @@ public: size_t get_size() const; GLuint get_binding_point() const; - // Upload data to the GPU buffer + // Upload data to the GPU buffer. Writes outside the buffer are rejected. + // data must follow the std140 layout of the matching shader block void update(const void* data, size_t size, size_t offset = 0) const; private: diff --git a/src/utils.hpp b/src/utils.hpp index 44280c3..894b986 100644 --- a/src/utils.hpp +++ b/src/utils.hpp @@ -17,6 +17,9 @@ Rc make_rc(Args&&... args) return std::make_shared(std::forward(args)...); } +template +using Weak = std::weak_ptr; + template using Opt = std::optional; diff --git a/src/vertex.hpp b/src/vertex.hpp index 1acf9bf..5e4e4d0 100644 --- a/src/vertex.hpp +++ b/src/vertex.hpp @@ -6,6 +6,7 @@ namespace Fan { +// Shader attribute locations: position 0, normal 1, texture_coords 2 struct Vertex { glm::vec3 position; diff --git a/src/window.cpp b/src/window.cpp index 7e15815..732e5af 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -5,6 +5,7 @@ #include #include "window.hpp" +#include "gldebug.hpp" #include "logger.hpp" namespace Fan @@ -23,6 +24,9 @@ Rc Window::create(int width, int height, const std::string& title) glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); +#ifndef NDEBUG + glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE); +#endif GLFWwindow *window = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr); if (window == nullptr) { @@ -43,6 +47,10 @@ Rc Window::create(int width, int height, const std::string& title) return nullptr; } +#ifndef NDEBUG + enable_gl_debug_output(); +#endif + int fb_width, fb_height; glfwGetFramebufferSize(window, &fb_width, &fb_height); glViewport(0, 0, fb_width, fb_height); diff --git a/src/window.hpp b/src/window.hpp index 90c9cb9..a32b4b1 100644 --- a/src/window.hpp +++ b/src/window.hpp @@ -10,6 +10,7 @@ namespace Fan { +// Owns GLFW, the OpenGL context and ImGui, so only one Window may exist class Window { public: @@ -25,10 +26,15 @@ public: bool should_close() const; bool is_valid() const; bool is_fullscreen() const; + // Key state is sampled in update(). Pressed is only true on the frame the + // key went down bool is_key_down(int glfw_key); bool is_key_pressed(int glfw_key); - float get_delta_time(); + float get_delta_time(); // Seconds between the last two update() calls + + // Window size in screen coordinates, which differ from pixels on HiDPI + // displays. Updated in end() int get_width() const; int get_height() const; @@ -37,12 +43,15 @@ public: void set_fullscreen(bool enabled); void set_vsync(bool enabled); - // User callbacks + // User callbacks. Cursor position is in screen coordinates and user_data is + // the pointer passed to set_callback_user_data() using MouseCallback = void (*)(double x_pos, double y_pos, void* user_data); void set_callback_user_data(void *data); void set_mouse_callback(MouseCallback callback); - // Main loop functions + // Main loop functions, call once per frame in this order. begin() starts the + // ImGui frame, update() samples time and keys, end() draws ImGui, swaps + // buffers and polls events void begin(); void update(); void end();