6
\$\begingroup\$

I am wondering if its possible to dynamically change shaders while the code is running. In my game, I want to have a development mode in which users can change the shader source and dynamically see their changes like so: https://www.youtube.com/watch?v=hlSG_iVC6xc this is my current reload method:

//Shader is my shader class and shader is the current shader
//delete all resources of previous shader

if (shader->GetProgram() != 0) 
    glDeleteProgram(shader->GetProgram());
if (inShader->GetVert() != 0) 
    glDeleteShader(inShader->GetVert());
if (inShader->GetGeom() != 0) 
    glDeleteShader(inShader->GetGeom());
if (inShader->GetFrag() != 0) 
    glDeleteShader(inShader->GetFrag());

GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);
GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
GLuint geometry_shader = glCreateShader(GL_GEOMETRY_SHADER);

//create vertex shader
glShaderSource(vertex_shader, 1, (const GLchar**)vertSource, NULL);
glCompileShader(vertex_shader);

//create fragment shader
glShaderSource(fragment_shader, 1, (const GLchar**)fragSource, NULL);
glCompileShader(fragment_shader);

GLuint program = glCreateProgram();

glAttachShader(program, vertex_shader);
glAttachShader(program, fragment_shader);
glLinkProgram(program);


Shader* theResult = new Shader(context);
theResult->SetVertShader(vertex_shader);
theResult->SetFragShader(fragment_shader);
theResult->SetGeomShader(geometry_shader);
theResult->SetProgram(program);

//replace the shader we created with the old one
*shader = *theResult;
delete theResult;
return shader;

The above code does one of two things when called, it either gives some undefined graphical behaviour or doesn't seem to reload the shader and use the previous ones. Is there something I'm missing regarding how I'm linking the new shaders, or is this even the right approach?

As requested i will add the shader class information:

//shader.h

protected:
/// Shader properties
unsigned int m_VertexShader;
unsigned int m_GeometricShader;
unsigned int m_FragmentShader;
unsigned int m_Program;
OpenGL* mContext;

public:
/// Constructor
Shader(OpenGL* context);
~Shader();

/// Getter/setters shader properties
inline unsigned int GetVert() { return m_VertexShader; }
inline unsigned int GetGeom() { return m_GeometricShader; }
inline unsigned int GetFrag() { return m_FragmentShader; }
inline unsigned int GetProgram() { return m_Program; }
inline void SetVertShader(unsigned int inValue) { m_VertexShader = inValue; }
inline void SetGeomShader(unsigned int inValue) { m_GeometricShader = inValue; }
inline void SetFragShader(unsigned int inValue) { m_FragmentShader = inValue; }
inline void SetProgram(unsigned int inValue) { m_Program = inValue; }

//shader.cpp

Shader::Shader(OpenGL* context)
: m_VertexShader(0),
m_GeometricShader(0),
m_FragmentShader(0),
m_Program(0),
mContext(context)
{}

Shader::~Shader()
{
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Your glDeleteProgram call should come after your glDeleteShader calls. Without knowing what your class members do it's difficult to say what you might be doing wrong. The fact that you're not just doing shader->Set* calls but instead creating an intermediate theResult object then copying it over then deleting it looks weird; what do your copy constructor and destructor do? It's possible your destructor is deleting the GL objects you've just created, but unless you show your class members we'll never be able to help you. \$\endgroup\$ – Maximus Minimus Jun 21 '16 at 21:09
  • \$\begingroup\$ I added the code you requested and made the changes you suggested, however it still does not seem to work. \$\endgroup\$ – Joshua Koza Jun 22 '16 at 13:34
  • 1
    \$\begingroup\$ @21stCenturyMoose Your first point (re: DeleteProgram after DeleteShader) is pure religion. While it comes from a good practice (Delete shaders as soon as you've linked them, because the program object will keep them alive), you haven't explained that part and so the advice by itself is only an unnecessary hassle. \$\endgroup\$ – MickLH Jun 22 '16 at 14:34
  • \$\begingroup\$ Do you use glUseProgram() somewhere in your code? \$\endgroup\$ – Shot Jun 23 '16 at 9:37
  • \$\begingroup\$ yea i do i know the shaders work its just when i want to reload them which is the above code it starts to bog down \$\endgroup\$ – Joshua Koza Jun 23 '16 at 12:20
4
\$\begingroup\$

I've done this recently, here is how I did it. It might not be the most efficient or smartest way to tackle this problem, but it has the job done. The code is not commented, just ask if something needs to be clarified.

Please not that in my real code I have a proxy enumeration between the shader types and the opengl ones (this is where ShaderType_Count comes from, and this is why I can use the shader type as array index, as-is this code will crash)

// Shader program stuff
struct Shader
{
    GLenum type;
    std::string filename;
    int id;
}

class ShaderProgram
{
public:
    template <typename Shaders...>
    void AddShaders(GLenum type, std::string name, Shaders... shaders) {
        AddShaders(type, name);
        AddShaders(shaders...);
    }

    void AddShaders(GLenum type, std::string name) {
        m_shaders.push_back(CompileShader(type, name));
    }

    void Link() {
        m_id = glCreateProgram();
        for (const auto& shader : m_shaders) {
            // OpenGL error if shader.id is 0
            glAttachShader(m_id, shader.id);
        }
        glLinkProgram(m_id);

        // Check link status
        // Detach and delete shaders
    }

    void Reload() {
        glDeleteProgram(m_id);

        // This is not efficient
        for (size_t i = 0; i < m_shaders.size(); ++i) {
            Shader new_shader = CompileShader(m_shaders[i].type, m_shaders[i].filename);
            if (new_shader.id != 0) {
                m_shaders[i] = new_shader;
            }
        }

        Link();
    }

private:
    Shader CompileShader(GLenum type, std::string filename) {
        Shader result;

        // 1. Load file content
        /* ... */

        // 2. Create shader and compile it
        result.id = glCreateShader(type);
        glShaderSource(result.id, 1, &shader_source, 0);
        glCompileShader(result.id);

        GLint compiled = GL_FALSE;
        glGetShaderiv(result.id, GL_COMPILE_STATUS, &compiled);
        if (compiled != GL_TRUE) {
            // Print error logs, etc
            result.id;
        }

        return result;
    }

private:
    GLuint m_id;
    std::vector<Shader> m_shaders;
};

// Reload stuff
class AssetManager {
public:
    AssetManager() {
        m_stopListening = false;
        m_fileChangesListener = std::thread(&AssetManager::ListenToFileChanges, this);
    }

    ~AssetManager() {
        m_stopListening = true;
        m_fileChangesListener.join();
    }

    ShaderProgram* AddShader(std::string name) {
        auto it = m_programs.insert(std::make_pair(name, ShaderProgram));
        return &it.first->second;
    }

    void ReloadData() {
        std::lock_guard<std::mutex> lock_guard(m_fileChangesMutex);
        for (auto program : m_dirtyPrograms) {
            program->Reload();
        }

        m_dirtyPrograms.clear();
    }

    void AssetManager::ListenToFileChanges()
    {
        // Available with VS2015, Recent GCC and Clang have it too
        namespace fs = std::experimental::filesystem;
        using namespace std::chrono_literals;

        while (!m_stopListening) {
            for (auto& program : m_programs) {
                ShaderTimeArray& shader_array = m_shadersLastTime[program.first];
                for (const auto& shader : shaders) {
                    // Check the last time the file was written
                    auto time = fs::last_write_time(fs::path(shader.filename)).time_since_epoch().count();
                    auto last_time = shader_array[shader.type];

                    // And compare it with the known time. 
                    if (last_time != 0 && last_time < time) {
                        // If they do not match, mark the program as needing a reload
                        m_dirtyPrograms.push_back(&program.second);
                    }
                    shader_array[shader.type] = time;
                }
            }

            // Just run this code once every second
            std::this_thread::sleep_for(1s);
        }
    }

private:
    using ShaderTimeArray = std::array<int64_t, ShaderType_Count>;
    std::map<std::string, ShaderProgram> m_programs;
    std::map<std::string, ShaderTimeArray> m_shadersLastTime;
    std::vector<ShaderProgram*> m_dirtyPrograms;
};

// Usage code
int main()
{
    AssetManager mgr;
    ShaderProgram* prg = mgr.AddShader("program");
    prg->AddShaders(GL_VERTEX_SHADER, "foo.vert", GL_FRAGMENT_SHADER, "foo.frag");

    while (true)
    {
        // Reload data that needs to
        mgr.ReloadData();
        // Update
        // Draw
    }

    return 0;
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.