Я довольно далеко продвинулся в разработке игры с использованием SDL, OpenGL и C++ и ищу способы оптимизировать то, как игра переключается между шейдерами GLSL для множества разных объектов разных типов. Это больше вопрос C++, чем вопрос OpenGL. Тем не менее, я все еще хочу предоставить как можно больше контекста, так как я чувствую, что необходимо некоторое обоснование того, почему предложенный класс Shader, который мне нужен, должен быть создан/удален таким, какой он есть.
Первые четыре раздела — это мои обоснования, путешествие и попытки, ведущие к этому моменту, однако на мой вопрос, скорее всего, можно ответить только в последнем разделе, и я намеренно написал его как нечто вроде tldr.
Необходимость класса Shader:
Я видел много онлайн-реализаций шейдеров OpenGL, которые создаются, компилируются и удаляются в одной и той же функции, когда игровые объекты создаются во время игры. Это оказалось неэффективным и слишком медленным в определенных частях моей игры. Таким образом, мне требовалась система, которая создает и компилирует шейдеры во время загрузки, а затем периодически использует/переключает их во время игры, а затем удаляет.
Это привело к созданию класса (Shader
), управляющего шейдерами OpenGL. Каждый экземпляр класса должен управлять одним уникальным шейдером OpenGL и содержать некоторое сложное поведение, связанное с типом шейдера, откуда он загружается, где он используется, какие юниформ-переменные он принимает и т. д.
При этом самая важная роль этого класса состоит в том, чтобы хранить переменную GLuint
id
, которая возвращается из glCreateShader()
, и управлять всеми вызовами OpenGL, которые относятся к шейдеру OpenGL, с помощью этого id
. Я понимаю, что это фактически бесполезно, учитывая глобальный характер OpenGL (поскольку в любом месте программы технически можно вызвать glDeleteShader()
с соответствующим id
и нарушить класс), однако в целях намеренной инкапсуляции всех вызовов OpenGL в очень определенные области на протяжении всего кодовой базы эта система резко уменьшит сложность кода.
С чего начинаются проблемы...
Самый автоматический способ управлять этим GLuint id
состоит в том, чтобы вызывать glCreateShader()
при создании объекта и glDeleteShader()
при его разрушении. Это гарантирует (в рамках OpenGL), что шейдер OpenGL будет существовать в течение всего времени существования объекта шейдера C++, и устраняет необходимость вызывать некоторые функции void createShader()
и deleteShader()
.
Это все хорошо, однако вскоре возникают проблемы при рассмотрении того, что произойдет, если этот объект будет скопирован. Что делать, если копия этого объекта уничтожена? Это означает, что glDeleteShader()
будет вызываться и эффективно ломать все копии объекта шейдера.
А как насчет простых ошибок вроде случайного вызова std::vector::push_back()
в векторе шейдеров? Различные методы std::vector
могут вызывать конструктор/конструктор копирования/деструктор своего типа, что может привести к той же проблеме, что и выше.
Хорошо, тогда... как насчет того, чтобы создать несколько методов void createShader()
и deleteShader()
, даже если это беспорядочно? К сожалению, это просто отсрочит указанную выше проблему, поскольку снова любые вызовы, изменяющие шейдер OpenGL, десинхронизируют/полностью сломают все копии класса шейдера с одним и тем же id
. Я ограничил вызовы OpenGL glCreateShader()
и glDeleteShader()
в этом примере, чтобы все было просто, однако я должен отметить, что в классе есть много других вызовов OpenGL, которые заставляют создавать различные экземпляры/статические переменные, которые слишком отслеживают копии экземпляров. сложно оправдать это таким образом.
Последнее замечание, которое я хочу сделать перед тем, как перейти к приведенному ниже дизайну классов, заключается в том, что для такого большого проекта, как необработанная игра на C++, OpenGL и SDL, я бы предпочел, чтобы любые потенциальные ошибки OpenGL, которые я делаю, генерировали ошибки компилятора, а не графические проблемы, которые являются сложнее отследить. Это может быть отражено в дизайне класса ниже.
Первая версия класса Shader
:
Именно по вышеуказанным причинам я выбрал:
- Сделайте конструктор
private
. - Предоставьте общедоступную функцию
static create
, которая вместо конструктора возвращает указатель на новый объект Shader. - Сделайте конструктор копирования
private
. - Сделайте
operator=
private
(хотя это может и не понадобиться). - Сделайте деструктор приватным.
- Поместите вызовы
glCreateShader()
в конструктор иglDeleteShader()
в деструктор, чтобы шейдеры OpenGL существовали на протяжении всего времени жизни этого объекта. - Поскольку функция
create
вызывает ключевое словоnew
(и возвращает указатель на него), место с внешним вызовомShader::create()
должно вызыватьdelete
вручную (подробнее об этом чуть позже).
Насколько я понимаю, первые два пункта используют фабричный шаблон и будут генерировать ошибку компилятора, если будет предпринята попытка создать тип класса, не являющийся указателем. Затем третий, четвертый и пятый маркеры предотвращают копирование объекта. Затем седьмой пункт гарантирует, что шейдер OpenGL будет существовать в течение того же времени жизни, что и объект шейдера C++.
Умные указатели и основная проблема:
Единственное, что я не очень люблю в вышеизложенном, это вызовы new
/delete
. Они также делают вызовы glDeleteShader()
в деструкторе объекта неуместными, учитывая инкапсуляцию, которую пытается достичь класс. Учитывая это, я решил:
- измените функцию
create
, чтобы она возвращалаstd::unique_ptr
типаShader
вместо указателяShader
.
Тогда функция create
выглядела так:
std::unique_ptr<Shader> Shader::create() {
return std::make_unique<Shader>();
}
Но затем возникла новая проблема... std::make_unique
, к сожалению, требует, чтобы конструктор был public
, что противоречит требованиям, описанным в предыдущем разделе. К счастью, я нашел решение, изменив его на:
std::unique_ptr<Shader> Shader::create() {
return std::unique_ptr<Shader>(new Shader());
}
Но... теперь std::unique_ptr
требует, чтобы деструктор был общедоступным! Это... лучше, но, к сожалению, это означает, что деструктор может быть вызван вручную вне класса, что, в свою очередь, означает, что функция glDeleteShader()
может быть вызвана вне класса.
Shader* p = Shader::create();
p->~Shader(); // Even though it would be hard to do this intentionally, I don't want to be able to do this.
delete p;
Финальный класс:
Для простоты я удалил большинство переменных экземпляра, аргументы функции/конструктора и другие атрибуты, но вот как выглядит окончательный предложенный класс (в основном):
class GLSLShader {
public:
~GLSLShader() { // OpenGL delete calls for id }; // want to make this private.
static std::unique_ptr<GLSLShader> create() { return std::unique_ptr<GLSLShader>(new GLSLShader()); };
private:
GLSLShader() { // OpenGL create calls for id };
GLSLShader(const GLSLShader& glslShader);
GLSLShader& operator=(const GLSLShader&);
GLuint id;
};
Меня все устраивает в этом классе, кроме того, что деструктор публичный. Я проверил этот дизайн, и увеличение производительности очень заметно. Несмотря на то, что я не могу себе представить, чтобы я когда-либо вручную вызывал деструктор объекта Shader
, мне не нравится, что он публично выставлен. Я также чувствую, что могу случайно что-то упустить, например, рассмотрение std::vector::push_back
во втором разделе.
Я нашел два возможных решения этой проблемы. Я хотел бы получить некоторые советы по этим или другим решениям.
Сделайте
std::unique_ptr
илиstd::make_unique
friend
классаShader
. Я читал такие темы, как этот, однако это делается для того, чтобы сделать конструктор доступным, а не деструктор. Я также не совсем понимаю недостатки / дополнительные соображения, необходимые для превращенияstd::unique_ptr
илиstd::make_unique
вfriend
(лучший ответ на эту тему + комментарии)?Не используйте умные указатели вообще. Возможно, есть способ, чтобы моя функция
static create()
возвращала необработанный указатель (с использованием ключевого словаnew
), который автоматически удаляется внутри класса/когдаShader
выходит за рамки и вызывается деструктор?
Большое спасибо за уделенное время.
std::shared_ptr
s? Разве это не решит все ваши проблемы? - person super   schedule 04.03.2021private:
. Или добавить друга. Или делать то, что, черт возьми, они хотят. @StoryTeller-UnslanderМоника прекрасно сказала: защищайтесь от Мерфи, а не от Макиавелли. В любом случае, кто вообще будет программировать этот интерфейс? Вы не совершите ошибку, вызвав деструктор вручную... - person davidbak   schedule 04.03.2021vector<Shader>
, вызов функцииpush_back()
для этого вектора вызовет деструкторы и сломает объекты. Я думал, что, возможно, есть какая-то другая вещь, о которой я не думаю, которая может автоматически вызывать деструктор, например, vector.push_back(), поэтому я подумал, что безопаснее найти способ сделать его приватным. Что вы думаете об этом @super? - person Jaymaican   schedule 04.03.2021GLSLShader
нельзя ни копировать, ни перемещать. Невозможно создать его экземпляры в векторе. Какpush_back
будет проблемой? - person StoryTeller - Unslander Monica   schedule 04.03.2021push_back
не будет проблемой. Я упомянул об этом в связи с тем, почему я изменил дизайн класса после второго раздела моего поста, поэтому я сказал, что если бы я не создавал свои объекты Shader так, как я это сделал. Меня беспокоили не векторы/push_back, а другая концепция, которую я не рассматривал с момента обновления дизайна. Может быть, что-то еще в STL, которое по-прежнему сможет вызывать деструктор, когда я этого не хочу, и т. д. - person Jaymaican   schedule 04.03.2021std::unique_ptr
, вы можете реализовать конструктор/назначение перемещения дляGLSLShader
(возможно, используяstd::optional<GLuint>
для определения недопустимого идентификатора). - person Jarod42   schedule 04.03.2021