[Godot] Optimizar codigo de función

AikonCWD

Tengo unas dudas sobre como mejorar el rendimiento de una función.

He sacado del cajón un código que hice sobre el "juego de la vida", que lo implementé usando el control TileMap pero eso es muy ineficiente, lento y pesado. Así que he querido simplificarlo lo máximo a ver si conseguía un rendimiento decente con un tablero grande de 180x100 (por ejemplo).

El rendimiento ha mejorado mucho, pero lo sigo encontrando leeeeento y me entran dudas de si la lentitud es porque lo estoy implementando mal o simplemente el Engine no da para más... Os he dejado una prueba de concepto:

https://aikoncwd.ovh/game-of-life/

En un tablero de 50x50 va muy fluido, entre 45 y 60 fps.
En un tablero de 100x100 la cosa petardea a 12fps...

Con la versión compilada de escritorio obtengo valores similares. El código lo dejo por aquí: https://pastebin.com/SZjc3ZHD


He cambiado el tilemap por llamadas directas a la api de dibujo (draw). No estoy instanciando objetos nuevos, ni cargando texturas ni nada. Pero sigue yendo lento.

He visto ejemplos más grandes de 300x300 que van muuuuuy finos, pero yo no lo consigo.
Estoy haciendo algo mal o simplemente es una limitación del Engine?

1 respuesta
carra

#1 No parece un código que debiera dar problemas para ir fluido. Leyéndolo se me ocurre alguna pequeña optimización pero no creo que hiciera una gran diferencia. Sospecho que puede tener que ver con cómo esté Python implementando las matrices (con qué estructuras de datos: arrays, listas, etc).

De todas formas, la función get_living_neighbors podrías optimizarla si a esas matrices les dejas una columa y fila extra vacías en cada extremo (por ejemplo si vas a 100x100, la matriz la haces de 102x102) y así te quitas todo ese cálculo de ver lo que pasa en los bordes, para cada pixel:

func get_living_neighbors(x, y):
    #función helper que cuenta celulas alrededor de una posición (x,y)
    #devuelve el número de celdas adyacentes
    var vecinos = 0
    for i in range(-1,2):
        for j in range(-1,2):
            if i != 0 and j != 0:
                vecinos += matrix[x + i][y + j]
    return vecinos
1 respuesta
carra

También podrías acelerar un poco el dibujado si prescindes de la función draw_cell así:

cell_colors = [Color(0,0,0), Color(1,1,1)]
 
func _draw():
    #recorremos el array_2d y pintamos las celdas
    for x in range(Globales.width):
        for y in range(Globales.height):
            draw_cell(x, y, cell_colors[matrix[x][y]] )
1 respuesta
AikonCWD

#2 Ese pequeño calculo me permite transformar la cuadrícula en un torus. De todas formas lo he probado tal y como has puesto y el resultado es el mismo, así que la pérdida de rendimiento no viene por ese cálculo.

#3 Bien visto. Lo he implementado y tampoco hay mejora alguna.


Lo que deduzco es que el motor pierde mucho tiempo al dibujar la matriz en pantalla (no en los cálculos). En lugar de dibujar cuadrados con draw_rect() he probado de dibujar circulitos con draw_circle(position, radius) y el rendimiento empeora x5.

Quizás representar el valor de una matriz con llamadas a draw() consume muchos recursos? De momento esto es lo que parece. Voy a cambiar el código para pintarlo de otra forma, por ejemplo instanciando sprites de colores... pero la lógica me dice que debería ir peor.

1 respuesta
Meleagant

#4 ¿Has probado a hacerlo con sprites? No sé cómo funciona Godot internamente con draw_rect, pero dibujar primitivas 2D es mucho más caro que dibujar sprites que al final son dos triángulos para la GPU.

1 respuesta
AikonCWD

#5 Justo a ello voy :)

Opté primero por no instanciar sprites y tirar de draw() porque me pareció menos costoso. Ahora en unos minutos edito y os digo.

1 1 respuesta
Meleagant

#6 Yo también pensaba que sería menos costoso cuando empecé, pero no. Y si lo piensas tiene sentido, un sprite al final ya tiene su bitmap generado en memoria, dibujar un rectángulo tiene que generarlo de nuevo cada vez, además de que las GPU están optimizadas para dibujar texturas en triángulos.

Seguramente sea eso.

1 respuesta
AikonCWD

#7 Con sprites parece que el proceso es algo más fluido, pero es muchísimo más lento de inicializar.

Es decir, en el momento de empezar, tengo que instanciar en tiempo de ejecución 100x100 objetos, posicionarlos en pantalla en forma de cuadrícula. Este proceso es lento hasta el punto que la ventana se queda congelada.

Una vez termina, el trabajo sobre esta matriz de sprites es algo más rápido y fluido. Voy a seguir probando diferentes formas a ver...

2 respuestas
carra

#8 ¿No puedes hacer que cada cuadrado sea un solo pixel en una textura/bitmap? Luego dibujas ese único bitmap con zoom aumentado y ya

1 respuesta
AikonCWD

#9 hummmm, con esto quizás? https://docs.godotengine.org/es/stable/classes/class_bitmap.html

Lo que tendría que mirar como pintar ese bitmap o asignarlo a un sprite. De momento he intentado usar la clase TileMap y es lo que mejor rendimiento tiene, pese a que no va fluido como otros ejemplos/implementaciones que he visto.

1 respuesta
Meleagant

#8 Joder, pues me parece una pasada que tarde tantísimo en instanciar 10.000 sprites.

Simplemente usando Pixi puedes instanciar 10.000 sobre la marcha sin notar ningún retardo.

https://www.goodboydigital.com/pixijs/bunnymark/

carra

#10 Pues no sé si sería eso en concreto, de Godot no controlo. Sorry

VorSandwich

No se como funciona exactamente Godot, pero a menos que la instrucción de draw_rect() se ignore cuando pintas un cuadrado negro/blanco en uno que ya era negro/blanco, estas haciendo muchos draws que realmente no necesitas.

Si quieres puedes hacer una prueba rápida con una matriz extra de bools para tener marcado si tienes que repintar o no y antes de pintar la compruebas.

2 1 respuesta
x0s3

Y no sería mejor en cuanto generas el rand en la matriz, si uno está vivo (1) te guardas esa posición.
entonces para pintar el primer fondo lo pintas directamente todo negro que seguro que hay alguna forma de poder hacerlo más rápido al ser todo lo mismo y luego para pintar los blancos con loopear solo los que eran vivos es suficiente así pintas mucho menos.
y en la actualización algo similar, solo te guardas las celdas que realmente han cambiado y entonces loopeas solo estas.

1 respuesta
S

has perfilado antes de intentar optimizar?

2 respuestas
AikonCWD

#15 no sé que es eso.

#13 #14 voy a implementar esto, que seguramente sea lo más eficiente. La pérdida de rendimiento la tengo cuando quiero dibujar toda la matriz en pantalla, en cada frame. Debería poder detectar únicamente los cambios y pintar los nuevos (que serán pocos en cada ciclo). Eso debería funcionar. Gracias chicos.


Como soy un cabeza hueca, me he empezado a ir por las ramas y he terminado implementando una hormiga de Langton para hacer los tests.

Podéis probarla aquí: https://aikoncwd.ovh/langton-ant/

Se pueden establecer el tamaño del mapa, número de hormigas y velocidad. A partir del ciclo 10.000 aparece el atractor típico de la hormiga, así que la implementación de este autómata es correcto.

A la noche quizás limpie el proyecto y suba un hilo explicando un poco en que consiste y por qué es tan fascinante.

1 respuesta
r2d2rigo

#16 como dice #15 aqui sin profiling no podemos ayudarte.

Donde esta el cuello de botella, en la CPU o en la GPU? Que hace draw_rect? Mi teoria es que estas dibujando sin batcheo ni nada y un tablero de 100x100 esta lanzando 10000 draw calls cada frame.

1 respuesta
AikonCWD

#17 aaah vale, se referia a eso xd.

El cuello está en la CPU, ya que todo el perfil de la GPU ni se entera.
draw_rect dibuja un rectangulo. Aquí hay una buena info en la entradilla y puede ser que no necesite llamar a _draw() en cada frame -> https://docs.godotengine.org/es/stable/classes/class_canvasitem.html

Estoy dibujando con batching. Te dejo 2 fotos, la primera sin batching y la segunda con batching


La rejilla era de 180x100. Sin batching meto 18000 calls al drawn y con batching bajo a los 1000 calls (a medida que las celulas van muriendo) pero empiezo haciendo apenas 4000 calls, así que el batching funciona.

Lo que me sorprende es que con batching tengo la misma latencia 120130ms que usando batching.

edit:

Acabo de descubrir la pestaña profiler en godot xd. No sé utilizar la herramienta pero bueno, dejo una foto:

1 respuesta
r2d2rigo

#18 tienes el hot path en get_living_neighbors.

Una optimizacion que se me ocurre es que en vez de iterar todo el tablero a cada frame, marques los vecinos de las celulas que cambian en el frame N para que en el frame N+1 se comprueben solo estas.

1

Usuarios habituales