¿Qué patrón de diseño se adapta mejor a la gestión de los identificadores de los objetos, sin pasar los identificadores o el Administrador?


8

Estoy escribiendo un juego en C ++ usando OpenGL.

Para aquellos que no saben, con la API de OpenGL haces muchas llamadas a cosas como glGenBuffersy glCreateShaderetc. Estos tipos de devolución GLuintson 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. ShaderProgrames mi clase que encapsula el enlace de GLuinta 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 WorldEntitynecesidades para saber lo ShaderProgramque necesita
  • Esto probablemente requiera algún tipo de clase de trago EntityManager que sepa qué instancia de qué ShaderProgrampasar a diferentes entidades.

Así que ahora porque hay una Managernecesidad de las clases para registrarse EntityManagercon la ShaderPrograminstancia que necesitan, o necesito un gran imbécil switchen el administrador que necesito actualizar para cada nuevo WorldEntitytipo derivado.

Mi primer pensamiento fue crear una ShaderManagerclase (lo sé, los gerentes son malos) que paso por referencia o puntero a las WorldEntityclases para que puedan crear lo ShaderProgramque quieran, a través de ShaderManagery ShaderManagerpueden realizar un seguimiento de los ShaderPrograms ya existentes , para que pueda devuelva uno que ya existe o cree uno nuevo si es necesario.

(Podría almacenar el ShaderProgramcorreo electrónico a través del hash de los nombres de archivo del ShaderProgramcódigo fuente real)

Y ahora:

  • Ahora estoy pasando punteros a en ShaderManagerlugar de ShaderProgram, por lo que todavía hay muchos parámetros
  • No necesito un EntityManager, las entidades mismas sabrán qué instancia ShaderProgramcrear, y ShaderManagermanejarán el ShaderPrograms real .
  • Pero ahora no sé cuándo ShaderManagerpuede eliminar de forma segura uno ShaderProgramque contiene.

Así que ahora he agregado un recuento de referencias a mi ShaderProgramclase que elimina su GLuintvía interna glDeleteProgramy la elimino ShaderManager.

Y ahora:

  • Un objeto puede crear lo ShaderProgramque necesita
  • Pero ahora hay duplicados ShaderProgramporque no hay un administrador externo que realice un seguimiento

Finalmente vengo a tomar una de dos decisiones:

1. Clase estática

A static classque se invoca para crear ShaderPrograms. Mantiene un seguimiento interno de ShaderPrograms basado en un hash de los nombres de archivo, esto significa que ya no necesito pasar punteros o referencias a ShaderPrograms o ShaderManagers, por lo que menos parámetros: WorldEntitiestienen todo el conocimiento sobre la instancia de ShaderProgramque quieren crear

Esta nueva static ShaderManagernecesita:

  • mantengo un recuento de la cantidad de veces que ShaderProgramse usa a y no hago ShaderProgramcopias O
  • ShaderPrograms cuenta sus referencias y solo llama glDeletePrograma su destructor cuando el recuento es 0Y ShaderManagerperiódicamente busca ShaderProgram's con un recuento de 1 y los descarta.

Las desventajas de este enfoque que veo son:

  1. Tengo una clase global estática que podría ser un problema. El contexto OpenGL debe crearse antes de invocar cualquier glXfunción. Por lo tanto, WorldEntitypodría crearse un e intentar crear uno ShaderProgramantes 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 IsContextCreatedque 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 Engineclase 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?

Y

  1. ¿Cuándo borrar realmente ShaderPrograms de la static ShaderManager? ¿Cada pocos minutos? Cada juego de bucle? Estoy manejando con gracia la recompilación de un sombreador en el caso en que ShaderProgramse eliminó un pero luego un nuevo lo WorldEntitysolicita; Pero estoy seguro de que hay una mejor manera.

2. Un mejor método

Eso es lo que pido aquí


2
Lo que viene a la mente cuando dice "Se necesitan muchos parámetros para construir una WorldEntity" es que se necesita un patrón de fábrica de algún tipo para manejar la conexión. Además, no estoy diciendo que necesariamente desee la inyección de dependencia aquí, pero si no ha mirado por ese camino antes, puede encontrarlo perspicaz. Los "gerentes" de los que está hablando aquí suenan similares a los controladores de alcance de por vida.
J Trana

Entonces, digamos que implemento una clase de fábrica para construir WorldEntitys; ¿No es eso cambiar algo del problema? Porque ahora la clase WorldFactory necesita pasar a cada WolrdEntity el ShaderProgram correcto.
NeomerArcana

Buena pregunta. A menudo, no, y he aquí por qué. En muchos casos, no tiene que tener un ShaderProgram específico, o es posible que desee cambiar cuál se instancia, o tal vez desee escribir una prueba unitaria con un ShaderProgram completamente simulado. Una pregunta que haría es: ¿realmente le importa a esa entidad qué programa de sombreador tiene? En algunos casos podría, pero dado que está utilizando un puntero ShaderProgram en lugar de un puntero MySpecificShaderProgram, puede que no. Además, el problema del alcance del Programa Shader ahora puede cambiar al nivel de fábrica, lo que permite cambios entre singletons, etc. fácilmente.
J Trana

Respuestas:


4
  1. Un mejor método. Eso es lo que pido aquí.

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 Modelque 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í oshared_ptrpara 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.

ShaderPrograms cuenta sus referencias y solo llama a glDeleteProgram en su destructor cuando el recuento es 0 Y ShaderManager comprueba periódicamente los ShaderProgram con un recuento de 1 y los descarta.

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_ptretc., sin meterme en problemas.

¿Cuándo borrar realmente ShaderPrograms del ShaderManager estático? ¿Cada pocos minutos? Cada juego de bucle? Estoy manejando con gracia la recompilación de un sombreador en el caso en que se eliminó un ShaderProgram pero luego un nuevo WorldEntity lo solicita; Pero estoy seguro de que hay una mejor manera.

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í.


1

En lugar de hacer un recuento de referencias en la ShaderProgramclase en sí, es mejor delegar eso a una clase de puntero inteligente, como std::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 ShaderProgramno se pueda copiar (constructor de copia privado / eliminado y operador de asignación de copia).
Para mantener un repositorio central de ShaderPrograminstancias que se puedan compartir, puede usar un SharedShaderProgramFactory(similar a su administrador estático, pero con un nombre mejor) como este:

class SharedShaderProgramFactory {
private:
  std::weak_ptr<ShaderProgram> program_a;

  std::shared_ptr<ShaderProgram> get_progam_a()
  {
    shared_ptr<ShaderProgram> temp = program_a.lock();
    if (!temp)
    {
      // Requested program does not currently exist, so (re-)create it
      temp = new ShaderProgramA();
      program_a = temp; // Save for future requests
    }
    return temp;
  }
};

La clase de fábrica se puede implementar como una clase estática, Singleton o una dependencia que se pasa donde sea necesario.


-3

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í:

"Cuando la complejidad de su escena aumente, tendrá más identificadores que se deben pasar alrededor del código"

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:

  1. No debe intentar poner un tipo o interfaz a la ruta de paso de datos. La razón es que este tipo sería inestable y requeriría cambios constantes cuando aumenta la complejidad de su escena.
  2. La ruta de paso de datos debe estar en la función main ().

Si le interesa saber por qué esto está atrayendo votos negativos. Como alguien que no está familiarizado con el tema, sería útil saber qué hay de malo en esta respuesta.
RubberDuck

1
No voté en contra aquí y curioso también, pero tal vez la idea de que OGL está diseñada en torno a todo el código que está dentro me mainparece un poco difícil (al menos en términos de redacción).
Dragon Energy
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.