From 1203b30cfdcd5db4bf983993c00cf56eb0d58075 Mon Sep 17 00:00:00 2001
From: Joseph Walton-Rivers <joseph@walton-rivers.uk>
Date: Wed, 22 Jun 2022 08:33:17 +0100
Subject: [PATCH] add 2D fallback shader

---
 fggl/gfx/ogl/renderer.cpp          |  15 ++--
 fggl/gfx/ogl/shader.cpp            | 134 +++++++++++++++++++++++------
 include/fggl/gfx/ogl/renderer.hpp  |   1 +
 include/fggl/gfx/ogl/shader.hpp    |  25 +++++-
 include/fggl/gfx/ogl4/fallback.hpp |  63 ++++++++++++++
 5 files changed, 205 insertions(+), 33 deletions(-)
 create mode 100644 include/fggl/gfx/ogl4/fallback.hpp

diff --git a/fggl/gfx/ogl/renderer.cpp b/fggl/gfx/ogl/renderer.cpp
index c65e7b5..09782d1 100644
--- a/fggl/gfx/ogl/renderer.cpp
+++ b/fggl/gfx/ogl/renderer.cpp
@@ -22,6 +22,7 @@
 #include <fggl/gfx/ogl/renderer.hpp>
 #include <fggl/gfx/paint.hpp>
 #include "fggl/data/model.hpp"
+#include "fggl/gfx/ogl4/fallback.hpp"
 
 #include <glm/ext/matrix_clip_space.hpp>
 #include <glm/ext/matrix_transform.hpp>
@@ -137,7 +138,7 @@ namespace fggl::gfx {
 	using data::Mesh2D;
 	using data::Vertex2D;
 
-	OpenGL4Backend::OpenGL4Backend(const Window &owner) : fggl::gfx::Graphics() {
+	OpenGL4Backend::OpenGL4Backend(const Window &owner) : fggl::gfx::Graphics(), m_canvasPipeline(INVALID_SHADER_ID) {
 		// initialise GLEW, or fail
 		// FIXME this binds the graphics stack to GLFW :'(
 		int version = gladLoadGLLoader( (GLADloadproc)glfwGetProcAddress );
@@ -166,8 +167,13 @@ namespace fggl::gfx {
 		m_cache = std::make_unique<ShaderCache>(storage);
 
 		// setup 2D rendering system
-		ShaderConfig shader2DConfig = ShaderFromName("shader2D");
-		m_cache->load(shader2DConfig);
+		ShaderConfig shader2DConfig = ShaderFromName("canvas");
+		m_canvasPipeline = m_cache->load(shader2DConfig);
+		if ( m_canvasPipeline == INVALID_SHADER_ID) {
+			debug::error("failed to load shader2D - using fallback");
+			m_canvasPipeline = m_cache->get(ogl4::FALLBACK_CANVAS_PIPELINE);
+		}
+
 		ShaderConfig shader3DConfig = ShaderFromName("phong");
 		m_cache->load(shader3DConfig);
 
@@ -191,8 +197,7 @@ namespace fggl::gfx {
 			return;
 		}
 
-		auto shader2D = m_cache->get("shader2D");
-		m_canvasRenderer->render(shader2D, paint);
+		m_canvasRenderer->render(m_canvasPipeline, paint);
 	}
 
 	void OpenGL4Backend::drawScene(ecs3::World &world) {
diff --git a/fggl/gfx/ogl/shader.cpp b/fggl/gfx/ogl/shader.cpp
index e43481c..5f9e251 100644
--- a/fggl/gfx/ogl/shader.cpp
+++ b/fggl/gfx/ogl/shader.cpp
@@ -14,6 +14,7 @@
 
 #include "fggl/gfx/ogl/types.hpp"
 #include <fggl/gfx/ogl/shader.hpp>
+#include "fggl/gfx/ogl4/fallback.hpp"
 
 #include <iostream>
 #include <vector>
@@ -21,17 +22,10 @@
 
 using namespace fggl::gfx;
 
-bool ShaderCache::compileShader(const std::string& fname, GLuint sid) {
-	std::string source;
-	bool result = m_storage->load(fggl::data::Data, fname, &source);
-	if ( !result ) {
-        spdlog::warn("could not load shader source from disk: {}", fname.c_str());
-		return false;
-	}
-
+bool ShaderCache::compileShaderFromSource(const std::string& source, GLuint sid) {
 	// upload and compile shader
-	const auto *src_cstr = (const GLchar *)source.c_str();
-	glShaderSource(sid, 1, &src_cstr, 0);
+	const char* src = source.c_str();
+	glShaderSource(sid, 1, &src, nullptr);
 	glCompileShader(sid);
 
 	// check it worked
@@ -44,15 +38,25 @@ bool ShaderCache::compileShader(const std::string& fname, GLuint sid) {
 		char* infoLog = new char[maxLength];
 		glGetShaderInfoLog(sid, maxLength, &maxLength, infoLog);
 
-        spdlog::warn("could not compile shader, '{}': {}", fname, infoLog);
+        spdlog::warn("could not compile shader source: {}", infoLog);
 		delete[] infoLog;
-
 		return false;
 	}
 
 	return true;
 }
 
+bool ShaderCache::readAndCompileShader(const std::string &filename, GLuint shader) {
+	std::string source;
+	bool result = m_storage->load(fggl::data::Data, filename, &source);
+	if ( !result ) {
+		spdlog::warn("could not load shader source from disk: {}", filename.c_str());
+		return false;
+	}
+
+	return compileShaderFromSource(source, shader);
+}
+
 ShaderCache::ShaderCache(std::shared_ptr<fggl::data::Storage> storage) : m_storage(storage), m_shaders(), m_binary(true) {
 
 	if ( !GLAD_GL_ARB_get_program_binary ) {
@@ -63,6 +67,7 @@ ShaderCache::ShaderCache(std::shared_ptr<fggl::data::Storage> storage) : m_stora
 	if ( GLAD_GL_ARB_shading_language_include ) {
 		setupIncludes();
 	}
+	initFallbackPipelines();
 }
 
 void ShaderCache::setupIncludes() {
@@ -80,14 +85,14 @@ void ShaderCache::setupIncludes() {
 
 }
 
-bool ShaderCache::loadFromDisk(GLuint pid, const ShaderConfig& config) {
+bool ShaderCache::loadFromDisk(GLuint pid, const std::string& pipelineName) {
 
 	BinaryCache cache;
-	auto fname = "shader_" + config.name + ".bin";
+	auto fname = "shader_" + pipelineName + ".bin";
 	bool status = m_storage->load( fggl::data::Cache, fname, &cache );
 
 	if ( !status ) {
-        spdlog::info("cached shader '{}' could not be loaded from disk", config.name);
+        spdlog::info("cached shader '{}' could not be loaded from disk", pipelineName);
 		return false;
 	}
 
@@ -96,12 +101,11 @@ bool ShaderCache::loadFromDisk(GLuint pid, const ShaderConfig& config) {
 	return result;
 }
 
-void ShaderCache::saveToDisk(GLuint pid, const ShaderConfig& config) {
-
+void ShaderCache::saveToDisk(GLuint pid, const std::string& pipelineName) {
 	BinaryCache cache;
 	cacheSave(pid, &cache);
 
-	auto fname = "shader_" + config.name + ".bin";
+	auto fname = "shader_" + pipelineName + ".bin";
 	m_storage->save( fggl::data::Cache, fname, &cache);
 }
 
@@ -125,7 +129,7 @@ GLuint ShaderCache::load(const ShaderConfig& config) {
 
 	if ( m_binary ) {
 		// if we have support for shader cache, give that a go
-		bool worked = loadFromDisk(pid, config);
+		bool worked = loadFromDisk(pid, config.name);
 		if ( worked ) {
 			m_shaders[config.name] = pid;
 			return pid;
@@ -136,17 +140,17 @@ GLuint ShaderCache::load(const ShaderConfig& config) {
 
 	// TODO actual shader loading
 	GLuint vertShader = glCreateShader(GL_VERTEX_SHADER);
-	compileShader(config.vertex, vertShader);
+	readAndCompileShader(config.vertex, vertShader);
 	glAttachShader(pid, vertShader);
 
 	GLuint fragShader = glCreateShader(GL_FRAGMENT_SHADER);
-	compileShader(config.fragment, fragShader);
+	readAndCompileShader(config.fragment, fragShader);
 	glAttachShader(pid, fragShader);
 
-	GLuint geomShader;
+	GLuint geomShader = 0;
 	if ( config.hasGeom ) {
 		geomShader = glCreateShader(GL_GEOMETRY_SHADER);
-		compileShader( config.geometry, geomShader );
+		readAndCompileShader( config.geometry, geomShader );
 		glAttachShader(pid, geomShader);
 	}
 
@@ -175,11 +179,11 @@ GLuint ShaderCache::load(const ShaderConfig& config) {
 			glDeleteShader( geomShader );
 		}
 
-		return false;
+		return INVALID_SHADER_ID;
 	}
 		
 	if ( m_binary ) {
-		saveToDisk(pid, config);
+		saveToDisk(pid, config.name);
 	}
 
 	// update the cache and return
@@ -187,6 +191,76 @@ GLuint ShaderCache::load(const ShaderConfig& config) {
 	return pid;
 }
 
+GLuint ShaderCache::load(const ShaderSources& sources, bool allowBinaryCache) {
+
+	spdlog::debug("starting shader program generation for {}", sources.name);
+
+	GLuint pid = glCreateProgram();
+
+	if ( m_binary && allowBinaryCache ) {
+		// if we have support for shader cache, give that a go
+		bool worked = loadFromDisk(pid, sources.name);
+		if ( worked ) {
+			m_shaders[sources.name] = pid;
+			return pid;
+		}
+
+		spdlog::debug("could not use cached shader for '{}', doing full compile.", sources.name);
+	}
+
+	// TODO actual shader loading
+	GLuint vertShader = glCreateShader(GL_VERTEX_SHADER);
+	compileShaderFromSource(sources.vertexSource, vertShader);
+	glAttachShader(pid, vertShader);
+
+	GLuint fragShader = glCreateShader(GL_FRAGMENT_SHADER);
+	compileShaderFromSource(sources.fragmentSource, fragShader);
+	glAttachShader(pid, fragShader);
+
+	GLuint geomShader = INVALID_SHADER_ID;
+	if ( !sources.geometrySource.empty() ) {
+		geomShader = glCreateShader(GL_GEOMETRY_SHADER);
+		compileShaderFromSource(sources.geometrySource, geomShader);
+		glAttachShader(pid, geomShader);
+	}
+
+	glLinkProgram(pid);
+	glDetachShader(pid, vertShader);
+	glDetachShader(pid, fragShader);
+
+	if ( geomShader != INVALID_SHADER_ID ) {
+		glDetachShader(pid, geomShader);
+	}
+
+	GLint linked = GL_FALSE;
+	glGetProgramiv(pid, GL_LINK_STATUS, &linked);
+	if ( linked == GL_FALSE ) {
+
+		// get the error
+		std::array<char, 512> infoLog;
+		glGetProgramInfoLog(pid, infoLog.size(), nullptr, infoLog.data());
+		spdlog::warn("linking shader program '{}' failed: {}", sources.name, infoLog.data());
+
+		// cleanup
+		glDeleteProgram( pid );
+		glDeleteShader( vertShader );
+		glDeleteShader( fragShader );
+		if ( geomShader != INVALID_SHADER_ID ) {
+			glDeleteShader( geomShader );
+		}
+
+		return INVALID_SHADER_ID;
+	}
+
+	if ( m_binary && allowBinaryCache ) {
+		saveToDisk(pid, sources.name);
+	}
+
+	// update the cache and return
+	m_shaders[ sources.name ] = pid;
+	return pid;
+}
+
 void ShaderCache::cacheSave(GLuint pid, BinaryCache* cache) {
 	GLsizei length;
 	glGetProgramiv(pid, GL_PROGRAM_BINARY_LENGTH, &length);
@@ -209,6 +283,16 @@ bool ShaderCache::cacheLoad(GLuint pid, const BinaryCache* cache) {
 	return status == GL_TRUE;
 }
 
+void ShaderCache::initFallbackPipelines() {
+	// canvas fallback pipeline
+	load({
+		.name = ogl4::FALLBACK_CANVAS_PIPELINE,
+		.vertexSource = ogl4::FALLBACK_CANVAS_VERTEX_SHADER,
+		.fragmentSource = ogl4::FALLBACK_CANVAS_FRAGMENT_SHADER,
+		.geometrySource = ""
+	}, false);
+}
+
 template<>
 bool fggl::data::fggl_deserialize(std::filesystem::path &data, fggl::gfx::BinaryCache *out) {
 	auto f = 
diff --git a/include/fggl/gfx/ogl/renderer.hpp b/include/fggl/gfx/ogl/renderer.hpp
index 93200ab..8abc93a 100644
--- a/include/fggl/gfx/ogl/renderer.hpp
+++ b/include/fggl/gfx/ogl/renderer.hpp
@@ -119,6 +119,7 @@ namespace fggl::gfx {
 			std::unique_ptr<ogl4::CanvasRenderer> m_canvasRenderer;
 			std::unique_ptr<ogl4::DebugRenderer> m_debugRenderer;
 			std::unique_ptr<ShaderCache> m_cache;
+			GLuint m_canvasPipeline;
 	};
 
 	using OpenGL4 = OpenGL4Backend;
diff --git a/include/fggl/gfx/ogl/shader.hpp b/include/fggl/gfx/ogl/shader.hpp
index 3c04b27..ba3bd18 100644
--- a/include/fggl/gfx/ogl/shader.hpp
+++ b/include/fggl/gfx/ogl/shader.hpp
@@ -25,6 +25,8 @@
 
 namespace fggl::gfx {
 
+	constexpr GLuint INVALID_SHADER_ID = 0;
+
 	struct ShaderConfig {
 		// required
 		std::string name;
@@ -36,6 +38,13 @@ namespace fggl::gfx {
 		bool hasGeom = false;
 	};
 
+	struct ShaderSources {
+		std::string name;
+		std::string vertexSource;
+		std::string fragmentSource;
+		std::string geometrySource;
+	};
+
 	inline ShaderConfig ShaderFromName(const std::string &name) {
 		return {
 			name,
@@ -56,10 +65,19 @@ namespace fggl::gfx {
 			~ShaderCache() = default;
 
 			GLuint load(const ShaderConfig &config);
+			GLuint load(const ShaderSources &sources, bool allowBinaryCache);
+
 			GLuint getOrLoad(const ShaderConfig &config);
 
 			GLuint get(const std::string &name);
 
+			/**
+			 * Fallback pipelines.
+			 *
+			 * All fallback pipelines are prefixed with "fallback_". They should be used as 'last resort' if there is
+			 * no matching shader on disk for this game.
+			 */
+			void initFallbackPipelines();
 
 		private:
 			std::shared_ptr<fggl::data::Storage> m_storage;
@@ -69,11 +87,12 @@ namespace fggl::gfx {
 			void setupIncludes();
 
 			// opengl operations
-			bool compileShader(const std::string &, GLuint);
+			bool readAndCompileShader(const std::string& filename, GLuint shader);
+			bool compileShaderFromSource(const std::string& source, GLuint);
 
 			// file io operations
-			bool loadFromDisk(GLuint pid, const ShaderConfig &config);
-			void saveToDisk(GLuint pid, const ShaderConfig &config);
+			bool loadFromDisk(GLuint pid, const std::string &pipelineName);
+			void saveToDisk(GLuint pid, const std::string &pipelineName);
 
 			bool m_binary;
 			void cacheSave(GLuint pid, BinaryCache *cache);
diff --git a/include/fggl/gfx/ogl4/fallback.hpp b/include/fggl/gfx/ogl4/fallback.hpp
new file mode 100644
index 0000000..b2fe7e4
--- /dev/null
+++ b/include/fggl/gfx/ogl4/fallback.hpp
@@ -0,0 +1,63 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 22/06/22.
+//
+
+#ifndef FGGL_GFX_OGL4_FALLBACK_HPP
+#define FGGL_GFX_OGL4_FALLBACK_HPP
+
+/**
+ * Fallback shaders.
+ *
+ * Embedded in the library, so that the user can use the library without having to include the shaders.
+ */
+namespace fggl::gfx::ogl4 {
+
+	constexpr const char* FALLBACK_CANVAS_PIPELINE = "fallback_canvas";
+
+	constexpr const char* FALLBACK_CANVAS_VERTEX_SHADER = R"glsl(
+		#version 330 core
+		layout (location = 0) in vec2 aPos;
+		layout (location = 1) in vec3 aColour;
+		layout (location = 2) in vec2 aTexPos;
+
+		uniform mat4 projection;
+
+		out vec3 colour;
+		out vec2 texPos;
+
+		void main() {
+			gl_Position = projection * vec4(aPos, 0.0, 1.0);
+			colour = aColour;
+			texPos = aTexPos;
+		})glsl";
+
+	constexpr const char* FALLBACK_CANVAS_FRAGMENT_SHADER = R"glsl(
+		#version 330 core
+		uniform sampler2D tex;
+
+		in vec3 colour;
+		in vec2 texPos;
+
+		out vec4 fragColour;
+
+		void main() {
+			fragColour = vec4(colour.xyz, texture(tex, texPos).r);
+		})glsl";
+
+} // namespace fggl::gfx::ogl4
+
+#endif //FGGL_GFX_OGL4_FALLBACK_HPP
-- 
GitLab