Estoy escribiendo un juego en C ++ usando OpenGL.
Para aquellos que no saben, con la API de OpenGL haces muchas llamadas a cosas como glGenBuffers
y glCreateShader
etc. Estos tipos de devolución GLuint
son identificadores únicos de lo que acabas de crear. Lo que se está creando vive en la memoria de la GPU.
Teniendo en cuenta que la memoria GPU a veces es limitada, no desea crear dos cosas que sean iguales cuando sean utilizadas por múltiples objetos.
Por ejemplo, Shaders. Vincula un programa de sombreado y luego tiene un GLuint
. Cuando hayas terminado con el Shader, deberías llamar glDeleteShader
(o algo por el estilo).
Ahora, digamos que tengo una jerarquía de clase superficial como:
class WorldEntity
{
public:
/* ... */
protected:
ShaderProgram* shader;
/* ... */
};
class CarEntity : public WorldEntity
{
/* ... */
};
class PersonEntity: public WorldEntity
{
/* ... */
};
Cualquier código que haya visto requeriría que todos los Constructores hayan ShaderProgram*
pasado para ser almacenados en el WorldEntity
. ShaderProgram
es mi clase que encapsula el enlace de GLuint
a al estado actual del sombreador en el contexto de OpenGL, así como algunas otras cosas útiles que debe hacer con los sombreadores.
El problema que tengo con esto es:
- Hay muchos parámetros necesarios para construir un
WorldEntity
(considere que puede haber una malla, un sombreador, un montón de texturas, etc., todos los cuales podrían compartirse, por lo que se pasan como punteros) - Lo que sea que esté creando las
WorldEntity
necesidades para saber loShaderProgram
que necesita - Esto probablemente requiera algún tipo de clase de trago
EntityManager
que sepa qué instancia de quéShaderProgram
pasar a diferentes entidades.
Así que ahora porque hay una Manager
necesidad de las clases para registrarse EntityManager
con la ShaderProgram
instancia que necesitan, o necesito un gran imbécil switch
en el administrador que necesito actualizar para cada nuevo WorldEntity
tipo derivado.
Mi primer pensamiento fue crear una ShaderManager
clase (lo sé, los gerentes son malos) que paso por referencia o puntero a las WorldEntity
clases para que puedan crear lo ShaderProgram
que quieran, a través de ShaderManager
y ShaderManager
pueden realizar un seguimiento de los ShaderProgram
s ya existentes , para que pueda devuelva uno que ya existe o cree uno nuevo si es necesario.
(Podría almacenar el ShaderProgram
correo electrónico a través del hash de los nombres de archivo del ShaderProgram
código fuente real)
Y ahora:
- Ahora estoy pasando punteros a en
ShaderManager
lugar deShaderProgram
, por lo que todavía hay muchos parámetros - No necesito un
EntityManager
, las entidades mismas sabrán qué instanciaShaderProgram
crear, yShaderManager
manejarán elShaderProgram
s real . - Pero ahora no sé cuándo
ShaderManager
puede eliminar de forma segura unoShaderProgram
que contiene.
Así que ahora he agregado un recuento de referencias a mi ShaderProgram
clase que elimina su GLuint
vía interna glDeleteProgram
y la elimino ShaderManager
.
Y ahora:
- Un objeto puede crear lo
ShaderProgram
que necesita - Pero ahora hay duplicados
ShaderProgram
porque no hay un administrador externo que realice un seguimiento
Finalmente vengo a tomar una de dos decisiones:
1. Clase estática
A static class
que se invoca para crear ShaderProgram
s. Mantiene un seguimiento interno de ShaderProgram
s basado en un hash de los nombres de archivo, esto significa que ya no necesito pasar punteros o referencias a ShaderProgram
s o ShaderManager
s, por lo que menos parámetros: WorldEntities
tienen todo el conocimiento sobre la instancia de ShaderProgram
que quieren crear
Esta nueva static ShaderManager
necesita:
- mantengo un recuento de la cantidad de veces que
ShaderProgram
se usa a y no hagoShaderProgram
copias O ShaderProgram
s cuenta sus referencias y solo llamaglDeleteProgram
a su destructor cuando el recuento es0
YShaderManager
periódicamente buscaShaderProgram
's con un recuento de 1 y los descarta.
Las desventajas de este enfoque que veo son:
Tengo una clase global estática que podría ser un problema. El contexto OpenGL debe crearse antes de invocar cualquier
glX
función. Por lo tanto,WorldEntity
podría crearse un e intentar crear unoShaderProgram
antes de la creación del contexto OpenGL, lo que provocará un bloqueo.La única forma de evitar esto es volver a pasar todo como punteros / referencias, o tener una clase global GLContext que se pueda consultar, o mantener todo en una clase que crea el Contexto en la construcción. O tal vez solo un booleano global
IsContextCreated
que se puede verificar. Pero me preocupa que esto me dé un código feo en todas partes.A lo que puedo ver la devolución es:
- La gran
Engine
clase que tiene todas las demás clases ocultas dentro de ella para que pueda controlar el orden de construcción / deconstrucción adecuadamente. Esto parece un gran lío de código de interfaz entre el usuario del motor y el motor, como un contenedor sobre un contenedor - Toda una serie de clases "Manager" que realizan un seguimiento de las instancias y eliminan cosas cuando es necesario. Esto podría ser un mal necesario?
- La gran
Y
- ¿Cuándo borrar realmente
ShaderProgram
s de lastatic ShaderManager
? ¿Cada pocos minutos? Cada juego de bucle? Estoy manejando con gracia la recompilación de un sombreador en el caso en queShaderProgram
se eliminó un pero luego un nuevo loWorldEntity
solicita; Pero estoy seguro de que hay una mejor manera.
2. Un mejor método
Eso es lo que pido aquí
fuente
WorldEntity
s; ¿No es eso cambiar algo del problema? Porque ahora la clase WorldFactory necesita pasar a cada WolrdEntity el ShaderProgram correcto.Respuestas:
Disculpas por la nigromancia, pero he visto a muchos tropezar con problemas similares con la administración de recursos de OpenGL, incluido yo en el pasado. Y muchas de las dificultades con las que luché, que reconozco en otros, provienen de la tentación de envolver y, a veces, abstraer e incluso encapsular los recursos OGL necesarios para que se represente alguna entidad de juego analógico.
Y la "mejor manera" que encontré (al menos una que terminó con mis luchas particulares allí) fue hacer las cosas al revés. Es decir, no te preocupes por los aspectos de bajo nivel de OGL en el diseño de las entidades y componentes de tu juego y aléjate de ideas como esa que tienes
Model
que almacenar como un triángulo y primitivas de vértices en forma de envoltura de objetos o incluso abstrayendo VBOs.Preocupaciones de renderización vs. Preocupaciones de diseño del juego
Hay conceptos de un nivel ligeramente más alto que las texturas de GPU, por ejemplo, con requisitos de administración más simples como imágenes de CPU (y los necesita de todos modos, al menos temporalmente, antes de que incluso pueda crear y vincular una textura de GPU). La ausencia de representación se refiere a que un modelo podría ser suficiente simplemente almacenando una propiedad que indica el nombre de archivo que se utilizará para el archivo que contiene los datos del modelo. Puede tener un componente "material" que sea de nivel superior y más abstracto y describa las propiedades de ese material que un sombreador GLSL.
Y luego solo hay un lugar en la base de código relacionado con cosas como sombreadores y texturas GPU y contextos VAO / VBO y OpenGL, y esa es la implementación del sistema de renderizado . El sistema de renderizado puede recorrer las entidades en la escena del juego (en mi caso, pasa por un índice espacial, pero puedes entenderlo más fácilmente y comenzar con un bucle simple antes de implementar optimizaciones como el sacrificio con un índice espacial), y descubre sus componentes de alto nivel como "materiales" e "imágenes" y nombres de archivos de modelos.
Y su trabajo es tomar esos datos de nivel superior que no están directamente relacionados con la GPU y cargar / crear / asociar / vincular / usar / desasociar / destruir los recursos necesarios de OpenGL en función de lo que descubre en la escena y lo que está sucediendo en el escena. Y eso elimina la tentación de usar cosas como singletons y versiones estáticas de "administradores" y demás, porque ahora toda la administración de recursos de OGL está centralizada en un sistema / objeto en su base de código (aunque, por supuesto, puede descomponerlo en otros objetos encapsulados) por el renderizador para hacer el código más manejable). También, naturalmente, evita algunos puntos de disparo con cosas como tratar de destruir recursos fuera de un contexto OGL válido,
Evitar cambios de diseño
Además, ofrece mucho espacio para respirar para evitar cambios costosos en el diseño central, porque digamos que, en retrospectiva, descubres que algunos materiales requieren pases de renderizado múltiples (y sombreadores múltiples) para renderizarse, como un pase de dispersión subsuperficial y sombreador para materiales de piel, mientras que anteriormente quería combinar un material con un solo sombreador de GPU. En ese caso, no hay cambios costosos en el diseño de las interfaces centrales utilizadas por muchas cosas. Todo lo que debe hacer es actualizar la implementación local del sistema de renderizado para manejar este caso anteriormente no anticipado cuando encuentra propiedades de máscara en su componente de material de nivel superior.
La estrategia general
Y esa es la estrategia general que uso ahora, y se vuelve cada vez más útil cuanto más complejas sean sus preocupaciones de renderizado. Como inconveniente, requiere un poco más de trabajo inicial que inyectar a sus entidades de juego con sombreadores y VBO y cosas así, y también combina su renderizador más con su motor de juego particular (o sus abstracciones, aunque a cambio del nivel superior Las entidades y conceptos del juego se desacoplan por completo de las preocupaciones de renderizado de bajo nivel). Y su procesador puede necesitar cosas como devoluciones de llamada para notificarlo cuando se destruyen entidades para que pueda desasociar y destruir cualquier dato que le asocie (puede usar el recuento de referencias aquí o
shared_ptr
para recursos compartidos, pero solo localmente dentro del renderizador). Y es posible que desee una forma eficiente de asociar y desasociar todo tipo de datos de representación a cualquier entidad en tiempo constante (un ECS tiende a proporcionar esto de forma inmediata a cada sistema con cómo puede asociar nuevos tipos de componentes sobre la marcha si tiene un ECS: si no, no debería ser demasiado difícil de ninguna manera) ... pero al revés, todo este tipo de cosas probablemente serán útiles para sistemas distintos al renderizador de todos modos.Es cierto que la implementación real se vuelve mucho más matizada que esto y podría desenfocar estas cosas un poco más, como su motor podría querer tratar cosas como triángulos y vértices en áreas distintas de la representación (por ejemplo: la física puede querer que tales datos detecten colisiones ) Pero donde la vida comenzó a ser mucho más fácil (al menos para mí) fue adoptar este tipo de inversión en la mentalidad y la estrategia como punto de partida.
Y diseñar un renderizador en tiempo real es muy difícil en mi experiencia: lo más difícil que he diseñado (y lo sigo rediseñando) con los rápidos cambios en el hardware, las capacidades de sombreado y las técnicas descubiertas. Pero este enfoque elimina la preocupación inmediata de cuándo se pueden crear / destruir los recursos de la GPU al centralizar todo eso en la implementación del renderizado, y aún más beneficioso para mí es que cambió lo que de otro modo sería costoso y los cambios de diseño en cascada (que podrían derramarse en código no inmediatamente relacionado con el renderizado) solo para la implementación del renderizador en sí. Y esa reducción en el costo del cambio puede sumar enormes ahorros con algo que cambia en los requisitos cada año o dos tan rápido como el procesamiento en tiempo real.
Tu ejemplo de sombreado
La forma en que abordo su ejemplo de sombreado es que no me preocupo por cosas como sombreadores GLSL en cosas como entidades de automóviles y personas. Me preocupo por los "materiales", que son objetos de CPU muy livianos que solo contienen propiedades que describen qué tipo de material es (piel, pintura de automóvil, etc.). En mi caso real, es un poco sofisticado ya que tengo un DSEL similar a Unreal Blueprints para programar sombreadores que usan un tipo de lenguaje visual, pero los materiales no almacenan los controles de sombreador GLSL.
Solía hacer cosas similares cuando almacenaba y administraba estos recursos "fuera del espacio" fuera del renderizador porque mis primeros intentos ingenuos que solo intentaban destruir directamente esos recursos en un destructor a menudo intentaban destruir esos recursos fuera de un contexto GL válido (y a veces incluso accidentalmente intentaba crearlos en un script o algo así cuando no estaba en un contexto válido), por lo que necesitaba diferir la creación y destrucción a los casos en los que podía garantizar que estaba en un contexto válido que conducen a diseños similares de "gerente" que usted describe.
Todos estos problemas desaparecen si está almacenando un recurso de CPU en su lugar y el procesador se ocupa de las preocupaciones de la gestión de recursos de la GPU. No puedo destruir un sombreador OGL en ningún lado, pero puedo destruir un material de CPU en cualquier lugar y usarlo fácilmente,
shared_ptr
etc., sin meterme en problemas.Ahora, esa preocupación es realmente complicada incluso en mi caso si desea administrar eficientemente los recursos de la GPU y descargarlos cuando ya no los necesite. En mi caso, puedo lidiar con escenas masivas y trabajo en efectos visuales en lugar de juegos en los que los artistas pueden tener contenido particularmente intenso no optimizado para el renderizado en tiempo real (texturas épicas, modelos que abarcan millones de polígonos, etc.).
Es muy útil para el rendimiento no solo para evitar renderizarlos cuando están fuera de la pantalla (fuera del entorno visual) sino también descargar los recursos de la GPU cuando ya no se necesitan por un tiempo (por ejemplo, el usuario no mira algo alejado) espacio por un tiempo).
Por lo tanto, la solución que suelo usar con más frecuencia es el tipo de solución "con marca de tiempo", aunque no estoy seguro de qué tan aplicable es con los juegos. Cuando empiezo a usar / enlazar recursos para renderizar (por ejemplo, pasan la prueba de eliminación de frustum), guardo la hora actual con ellos. Luego, periódicamente se realiza una verificación para ver si esos recursos no se han utilizado durante un tiempo, y si es así, se descargan / destruyen (aunque los datos originales de la CPU utilizados para generar el recurso GPU se mantienen hasta que la entidad real que almacena esos componentes se destruye) o hasta que esos componentes se eliminen de la entidad). A medida que aumenta el número de recursos y se usa más memoria, el sistema se vuelve más agresivo con respecto a la descarga / destrucción de esos recursos (la cantidad de tiempo de inactividad permitido para un viejo,
Me imagino que depende mucho del diseño de tu juego. Dado que si tiene un juego con un enfoque más segmentado con niveles / zonas más pequeños, entonces podría (y encontrar el tiempo más fácil para mantener estables las tasas de cuadros) cargar todos los recursos necesarios para ese nivel por adelantado y descargarlos cuando el usuario pasa al siguiente nivel. Mientras que si tienes un juego masivo de mundo abierto que sea perfecto de esa manera, es posible que necesites una estrategia mucho más sofisticada para controlar cuándo crear y destruir estos recursos, y puede haber un gran desafío para hacerlo sin tartamudear. En mi dominio de efectos visuales, un pequeño inconveniente con las velocidades de fotogramas no es tan importante (trato de eliminarlos dentro de lo razonable) ya que el usuario no va a terminar el juego como resultado de ello.
Toda esta complejidad en mi caso todavía está aislada del sistema de representación, y aunque he generalizado las clases y el código para ayudar a implementarlo, no hay preocupaciones sobre contextos GL válidos y tentaciones para usar globales o algo así.
fuente
En lugar de hacer un recuento de referencias en la
ShaderProgram
clase en sí, es mejor delegar eso a una clase de puntero inteligente, comostd::shared_ptr<>
. De esa manera, se asegura de que cada clase solo tenga un trabajo único que hacer.Para evitar agotar accidentalmente sus recursos de OpenGL, puede hacer que
ShaderProgram
no se pueda copiar (constructor de copia privado / eliminado y operador de asignación de copia).Para mantener un repositorio central de
ShaderProgram
instancias que se puedan compartir, puede usar unSharedShaderProgramFactory
(similar a su administrador estático, pero con un nombre mejor) como este:La clase de fábrica se puede implementar como una clase estática, Singleton o una dependencia que se pasa donde sea necesario.
fuente
Opengl ha sido diseñado como una biblioteca C y tiene las características de un software de procedimiento. Una de las reglas de opengl que proviene de ser una biblioteca C se ve así:
Esta es una característica de la API de opengl. Básicamente se supone que todo el código está dentro de la función main (), y todos esos controladores se pasan a través de las variables locales de main ().
Las consecuencias de esta regla son las siguientes:
fuente
main
parece un poco difícil (al menos en términos de redacción).