Entendemos las características del subsistema gráfico de los microcontroladores.

¡Hola!



En este artículo, me gustaría hablar sobre las características de la implementación de una interfaz gráfica de usuario con widgets en un microcontrolador y cómo tener una interfaz de usuario familiar y un FPS decente. Me gustaría centrarme no en una biblioteca de gráficos específica, sino en cosas generales: memoria, caché del procesador, dma, etc. Como soy un desarrollador del equipo de Embox , los ejemplos y experimentos estarán en este sistema operativo RT.





Anteriormente ya hablamos sobre ejecutar la biblioteca Qt en un microcontrolador . La animación resultó ser bastante fluida, pero al mismo tiempo los costos de memoria incluso para almacenar el firmware fueron significativos: el código se ejecutó desde la memoria flash QSPI externa. Eso sí, cuando se requiere una interfaz compleja y multifuncional, que también sepa hacer algún tipo de animación, entonces el costo de los recursos de hardware puede estar bastante justificado (sobre todo si ya tienes este código desarrollado para Qt).



Pero, ¿y si no necesita todas las funciones de Qt? ¿Qué pasa si tiene cuatro botones, un control de volumen y un par de menús emergentes? Al mismo tiempo, querrás que “se vea bien y funcione rápido” :) Entonces será recomendable usar herramientas más ligeras, por ejemplo, la biblioteca lvgl o similar.



En nuestro proyecto Embox hace algún tiempo, se transfirió Nuklear : un proyecto para crear una biblioteca muy liviana que consta de un encabezado y le permite crear fácilmente una GUI simple. Decidimos usarlo para crear una pequeña aplicación en la que habrá un widget con un conjunto de elementos gráficos y que podría controlarse a través de una pantalla táctil.



Se eligió como plataforma STM32F7-Discovery con Cortex-M7 y pantalla táctil.



Primeras optimizaciones. Guardar memoria



Entonces, se selecciona la biblioteca de gráficos, al igual que la plataforma. Ahora entendamos cuáles son los recursos. Vale la pena señalar aquí que la memoria principal SRAM es muchas veces más rápida que la SDRAM externa, por lo que si los tamaños de pantalla lo permiten, entonces, por supuesto, es mejor poner el framebuffer en SRAM. Nuestra pantalla tiene una resolución de 480x272. Si queremos un color de 4 bytes por píxel, obtenemos unos 512 KB. Al mismo tiempo, el tamaño de la RAM interna es de solo 320 e inmediatamente queda claro que la memoria de video será externa. Otra opción es reducir la profundidad de bits de color a 16 (es decir, 2 bytes) y así reducir el consumo de memoria a 256 KB, que ya puede caber en la RAM principal.



Lo primero que puedes intentar es ahorrar en todo. Hagamos un búfer de video de 256 KB, colóquelo en la RAM y dibujemos en él. El problema que encontramos de inmediato fue el "parpadeo" de la escena que se produce al dibujar directamente en la memoria de vídeo. Nuklear vuelve a dibujar toda la escena desde cero, por lo que cada vez que se llena toda la pantalla primero, luego se dibuja el widget, luego se coloca un botón en el que se coloca el texto, y así sucesivamente. Como resultado, el ojo humano puede ver cómo se vuelve a dibujar toda la escena y la imagen "parpadea". Es decir, una simple ubicación en la memoria interna no se guarda.



Tampón intermedio. Optimizaciones del compilador. FPU



Después de jugar con el método anterior (ubicación en la memoria interna) por un momento, los recuerdos de X Server y Wayland inmediatamente comenzaron a venir a la mente. Sí, de hecho, de hecho, los administradores de ventanas se dedican a procesar las solicitudes de los clientes (solo nuestra aplicación personalizada) y luego recopilar los elementos en la escena final. Por ejemplo, el kernel de Linux envía eventos desde los dispositivos de entrada al servidor a través del controlador evdev. El servidor, a su vez, determina a qué cliente atender el evento. Los clientes, habiendo recibido un evento (por ejemplo, presionando en una pantalla táctil), ejecutan su lógica interna: resaltan el botón, muestran un nuevo menú. Además (ligeramente diferente para X y Wayland), el cliente o el servidor dibujan los cambios en el búfer. Y luego el compositor está juntando todas las piezas para dibujar en la pantalla.Explicación bastante simple y esquemática aquíaquí .



Quedó claro que necesitamos una lógica similar, pero realmente no queremos insertar X Server en stm32 por el bien de una aplicación pequeña. Por lo tanto, intentemos dibujar no en la memoria de video, sino en la memoria ordinaria. Después de renderizar toda la escena, copiará el búfer a la memoria de video.



Código de widget
        if (nk_begin(&rawfb->ctx, "Demo", nk_rect(50, 50, 200, 200),
            NK_WINDOW_BORDER|NK_WINDOW_MOVABLE|
            NK_WINDOW_CLOSABLE|NK_WINDOW_MINIMIZABLE|NK_WINDOW_TITLE)) {
            enum {EASY, HARD};
            static int op = EASY;
            static int property = 20;
            static float value = 0.6f;

            if (mouse->type == INPUT_DEV_TOUCHSCREEN) {
                /* Do not show cursor when using touchscreen */
                nk_style_hide_cursor(&rawfb->ctx);
            }

            nk_layout_row_static(&rawfb->ctx, 30, 80, 1);
            if (nk_button_label(&rawfb->ctx, "button"))
                fprintf(stdout, "button pressed\n");
            nk_layout_row_dynamic(&rawfb->ctx, 30, 2);
            if (nk_option_label(&rawfb->ctx, "easy", op == EASY)) op = EASY;
            if (nk_option_label(&rawfb->ctx, "hard", op == HARD)) op = HARD;
            nk_layout_row_dynamic(&rawfb->ctx, 25, 1);
            nk_property_int(&rawfb->ctx, "Compression:", 0, &property, 100, 10, 1);

            nk_layout_row_begin(&rawfb->ctx, NK_STATIC, 30, 2);
            {
                nk_layout_row_push(&rawfb->ctx, 50);
                nk_label(&rawfb->ctx, "Volume:", NK_TEXT_LEFT);
                nk_layout_row_push(&rawfb->ctx, 110);
                nk_slider_float(&rawfb->ctx, 0, &value, 1.0f, 0.1f);
            }
            nk_layout_row_end(&rawfb->ctx);
        }
        nk_end(&rawfb->ctx);
        if (nk_window_is_closed(&rawfb->ctx, "Demo")) break;

        /* Draw framebuffer */
        nk_rawfb_render(rawfb, nk_rgb(30,30,30), 1);

        memcpy(fb_info->screen_base, fb_buf, width * height * bpp);




Este ejemplo crea una ventana de 200 x 200 px y dibuja gráficos en ella. La escena final en sí se dibuja en el búfer fb_buf, que asignamos a SDRAM. Y luego, en la última línea, simplemente se llama a memcpy. Y todo se repite en un ciclo sin fin.



Si solo compilamos y ejecutamos este ejemplo, obtenemos entre 10 y 15 FPS. Lo que ciertamente no es muy bueno, porque se nota incluso a simple vista. Además, dado que el código de renderizado de Nuklear contiene muchos cálculos de punto flotante, habilitamos su soporte inicialmente , sin él, el FPS habría sido aún menor. La primera y más simple (gratuita) optimización es, por supuesto, el indicador del compilador -O2.



Construyamos y ejecutemos el mismo ejemplo: obtenemos 20 FPS. Mejor, pero aún no lo suficiente para un buen trabajo.



Habilitación de cachés de procesador. Modo de escritura simultánea



Antes de pasar a otras optimizaciones, diré que estamos usando el complemento rawfb como parte de Nuklear, que se basa directamente en la memoria. En consecuencia, la optimización de la memoria parece muy prometedora. Lo primero que me viene a la mente es el caché.



En versiones anteriores de Cortex-M, como Cortex-M7 (nuestro caso), se incorpora un caché de procesador adicional (caché de instrucciones y caché de datos). Se habilita a través del registro CCR del Bloque de Control del Sistema. Pero con la inclusión del caché surgen nuevos problemas: la inconsistencia de los datos en el caché y la memoria. Hay varias formas de administrar la caché, pero en este artículo no me detendré en ellas, por lo que pasaré a una de las más simples, en mi opinión. Para resolver el problema de inconsistencia de la memoria caché / memoria, simplemente podemos marcar toda la memoria disponible como "no almacenable en caché". Esto significa que todas las escrituras en esta memoria siempre irán a la memoria y no al caché. Pero si marcamos toda la memoria de esta manera, tampoco tendrá sentido en la caché. Existe otra opción. Este es un modo de "transferencia", en el que todas las escrituras en la memoria marcadas como escritura directa se envían simultáneamente a la caché.y en la memoria. Esto crea una sobrecarga de escritura, pero por otro lado, acelera enormemente la lectura, por lo que el resultado dependerá de la aplicación específica.



Para Nuklear, el modo de escritura directa resultó ser muy bueno: el rendimiento aumentó de 20 FPS a 45 FPS, lo que en sí mismo ya es bastante bueno y fluido. El efecto es ciertamente interesante, incluso intentamos deshabilitar el modo de escritura directa, sin prestar atención a la inconsistencia de los datos, pero el FPS aumentó solo a 50 FPS, es decir, no hubo un aumento significativo en comparación con la escritura. De esto llegamos a la conclusión de que nuestra aplicación requiere muchas operaciones de lectura, no escrituras. La pregunta es, por supuesto, ¿dónde? Quizás debido a la cantidad de transformaciones en el código rawfb, que a menudo acceden a la memoria para leer el siguiente coeficiente o algo así.



Doble búfer (hasta ahora con un búfer intermedio). Habilitando DMA



No quería detenerme a 45 FPS, así que decidimos experimentar más. La siguiente idea fue el doble búfer. La idea es muy conocida y, en general, sencilla. Dibujamos la escena usando un dispositivo en un búfer, mientras que el otro dispositivo se muestra desde otro búfer. Si observa el código anterior, puede ver claramente un bucle en el que la escena se dibuja primero en el búfer y luego el contenido se copia en la memoria de video usando memcpy. Está claro que memcpy usa CPU, es decir, la renderización y la copia ocurren secuencialmente. Nuestra idea era que la copia se pudiera hacer en paralelo usando DMA. En otras palabras, mientras el procesador dibuja una nueva escena, el DMA copia la escena anterior en la memoria de video.



Memcpy se reemplaza con el siguiente código:



            while (dma_in_progress()) {
            }

            ret = dma_transfer((uint32_t) fb_info->screen_base,
                    (uint32_t) fb_buf[fb_buf_idx], (width * height * bpp) / 4);
            if (ret < 0) {
                printf("DMA transfer failed\n");
            }

            fb_buf_idx = (fb_buf_idx + 1) % 2;


Aquí se ingresa fb_buf_idx, el índice del búfer. fb_buf_idx = 0 es el búfer frontal, fb_buf_idx = 1 es el búfer posterior. La función dma_transfer () toma destino, origen y un número de palabras de 32 bits. Luego, el DMA se carga con los datos requeridos y el trabajo continúa con el siguiente búfer.



Habiendo probado este mecanismo, el rendimiento aumentó a unos 48 FPS. Un poco mejor que memcpy (), pero solo un poco. No quiero decir que DMA resultó ser inútil, pero en este ejemplo en particular, el efecto de la caché en el panorama general fue mejor.



Después de una pequeña sorpresa de que DMA funcionó peor de lo esperado, se nos ocurrió una idea “excelente”, como nos pareció entonces, de usar varios canales DMA. ¿Cuál es el punto de? La cantidad de datos que se pueden cargar en DMA a la vez en stm32f7xx es de 256 KB. Al mismo tiempo, recuerde que nuestra pantalla es de 480x272 y la memoria de video es de aproximadamente 512 KB, lo que significa que parece que puede poner la primera mitad de los datos en un canal DMA y la segunda mitad en el segundo. Y todo parece ir bien ... Pero el rendimiento baja de 48 FPS a 25-30 FPS. Es decir, volvemos a la situación en la que aún no se ha habilitado la caché. ¿Con qué se puede conectar? De hecho, debido al hecho de que el acceso a la memoria SDRAM está sincronizado, incluso la memoria se llama Memoria de acceso aleatorio dinámico síncrono (SDRAM), por lo que esta opción solo agrega sincronización adicional,sin hacer la escritura en la memoria en paralelo, como se desee. Después de una pequeña reflexión, nos dimos cuenta de que no hay nada sorprendente aquí, porque la memoria es una, y los ciclos de escritura y lectura se generan en un microcircuito (en un bus), y como se agrega otra fuente / receptor, entonces el árbitro, que resuelve las llamadas en el bus. , necesita mezclar ciclos de comando de diferentes canales DMA.



Doble búfer. Trabajando con LTDC



Copiar desde un búfer intermedio es ciertamente bueno, pero como descubrimos, esto no es suficiente. Echemos un vistazo a otra mejora obvia: el doble búfer. En la gran mayoría de los controladores de pantalla modernos, puede configurar la dirección de la memoria de video utilizada. Por lo tanto, puede evitar copiar por completo y simplemente reorganizar la dirección de la memoria de video en el búfer preparado, y el controlador de pantalla tomará los datos de la manera óptima por sí solo a través de DMA. Se trata de un doble búfer real, sin un búfer intermedio como antes. También hay una opción cuando el controlador de pantalla puede tener dos o más búferes, que es esencialmente lo mismo: escribimos en un búfer y el controlador usa el otro, mientras que no es necesario copiar.



El LTDC (controlador de pantalla LCD-TFT) en stm32f74xx tiene dos capas de superposición de hardware: Capa 1 y Capa 2, donde la Capa 2 se superpone a la Capa 1. Cada una de las capas se puede configurar de forma independiente y se puede activar o desactivar por separado. Intentamos habilitar solo la Capa 1 y reorganizar la dirección de la memoria de video en el búfer frontal o posterior. Es decir, le damos uno al display, y en el otro dibujamos en este momento. Pero tenemos un jitter notable al cambiar de superposición.



Probamos la opción cuando usamos ambas capas con una de ellas encendida / apagada, es decir, cuando cada capa tiene su propia dirección de memoria de video, que no cambia, y el búfer se cambia encendiendo una de las capas mientras apaga la otra. La variación también resultó en jitter. Y finalmente, probamos la opción cuando la capa no estaba apagada, pero el canal alfa estaba configurado en cero 0 o máximo (255), es decir, controlamos la transparencia, haciendo invisible una de las capas. Pero esta opción no estuvo a la altura de las expectativas, el temblor seguía presente.



La razón no estaba clara: la documentación dice que las actualizaciones del estado de la capa se pueden realizar sobre la marcha. Hicimos una prueba simple: apagamos los cachés, en coma flotante, dibujamos una imagen estática con un cuadrado verde en el centro de la pantalla, lo mismo para la Capa 1 y la Capa 2, y comenzamos a cambiar los niveles en un bucle, esperando obtener una imagen estática. Pero volvimos a tener el mismo batido.



Quedó claro que era otra cosa. Y luego recordamos la alineación de la dirección del framebuffer en la memoria. Dado que los búferes se asignaron desde el montón y sus direcciones no estaban alineadas, alineamos sus direcciones en 1 KB: obtuvimos la imagen esperada sin fluctuación. Luego encontraron en la documentación que LTDC resta datos en lotes de 64 bytes, y que la irregularidad de los datos produce una pérdida significativa de rendimiento. En este caso, tanto la dirección del inicio del framebuffer como su ancho deben estar alineados. Para probar, cambiamos el ancho de 480x4 a 470x4, que no es divisible por 64 bytes, y obtuvimos el mismo jitter.



Como resultado, alineamos ambos búferes en 64 bytes, nos aseguramos de que el ancho también estuviera alineado en 64 bytes y ejecutamos nuklear: el jitter desapareció. La solución que funcionó se ve así. En lugar de cambiar entre capas deshabilitando completamente la Capa 1 o la Capa, use la transparencia. Es decir, para deshabilitar el nivel, establezca su transparencia en 0 y para habilitarlo, en 255.



        BSP_LCD_SetTransparency_NoReload(fb_buf_idx, 0xff);

        fb_buf_idx = (fb_buf_idx + 1) % 2;

        BSP_LCD_SetTransparency(fb_buf_idx, 0x00);


¡Tenemos 70-75 FPS! Mucho mejor que el original 15.



Cabe señalar que la solución funciona a través del control de transparencia, y las opciones de deshabilitar uno de los niveles y la opción de reorganizar la dirección de nivel dan una fluctuación de imagen en FPS grandes 40-50, la razón actualmente es desconocida para nosotros. Además, avanzando, diré que esta es una solución para esta placa.



Relleno de escena de hardware a través de DMA2D



Pero este no es el límite, nuestra última optimización para aumentar FPS es el llenado de escenas de hardware. Antes de eso, hicimos el llenado programáticamente:

nk_rawfb_render(rawfb, nk_rgb(30,30,30), 1);


Digamos ahora al complemento rawfb que no es necesario llenar la escena, solo pintar sobre:

nk_rawfb_render(rawfb, nk_rgb(30,30,30), 0);


Rellenaremos la escena con el mismo color 0xff303030, solo en hardware a través del controlador DMA2D. Una de las principales funciones de DMA2D es copiar o rellenar un rectángulo en la RAM. La principal conveniencia aquí es que no se trata de una pieza de memoria continua, sino de un área rectangular, que se encuentra en la memoria con interrupciones, lo que significa que el DMA ordinario no se puede realizar de inmediato. En Embox, todavía no hemos trabajado con este dispositivo, así que usemos las herramientas STM32Cube: la función BSP_LCD_Clear (uint32_t Color). Programa el color de relleno y el tamaño de toda la pantalla en DMA2D.



Período de supresión vertical (VBLANK)



Pero incluso a 80 FPS, seguía habiendo un problema notable: partes del widget se movían con pequeños "descansos" cuando se movían por la pantalla. Es decir, el widget parecía estar dividido en 3 (o más) partes que se movían una al lado de la otra, pero con un ligero retraso. Resultó que la razón fue una actualización incorrecta de la memoria de video. Más precisamente, actualizaciones en los intervalos de tiempo incorrectos.



El controlador de pantalla tiene una propiedad como VBLANK, también es VBI o Vertical Blanking Period . Denota el intervalo de tiempo entre cuadros de video adyacentes. O más precisamente, el tiempo entre la última línea del fotograma de video anterior y la primera línea del siguiente. En este intervalo, no se transfieren datos nuevos a la pantalla, la imagen es estática. Por esta razón, es seguro actualizar la memoria de video dentro de VBLANK.



En la práctica, el controlador LTDC tiene una interrupción que está configurada para activarse después de procesar la siguiente línea de memoria intermedia (registro de configuración de posición de interrupción de línea LTDC (LTDC_LIPCR)). Por lo tanto, si configura esta interrupción en el último número de línea, solo obtendremos el comienzo del intervalo VBLANK. En este punto, realizamos el cambio de búfer necesario.



Como resultado de tales acciones, la imagen volvió a la normalidad, los huecos desaparecieron. Pero al mismo tiempo, el FPS cayó de 80 a 60. Entendamos cuál podría ser la razón de este comportamiento.



La siguiente fórmula se puede encontrar en la documentación :



          LCD_CLK (MHz) = total_screen_size * refresh_rate,


donde tamaño_pantalla_total = ancho_total x altura_total. LCD_CLK es la frecuencia a la que el controlador de pantalla cargará píxeles desde la memoria de video a la pantalla (por ejemplo, a través de la Interfaz serie de pantalla (DSI)). Pero refresh_rate ya es la frecuencia de actualización de la pantalla en sí, su característica física. Resulta que, conociendo la frecuencia de actualización de la pantalla y sus dimensiones, puede configurar la frecuencia para el controlador de pantalla. Después de verificar los registros de la configuración que crea el STM32Cube, descubrimos que sintoniza el controlador a una pantalla de 60 Hz. Así que todo se juntó.



Un poco sobre los dispositivos de entrada en nuestro ejemplo



Volvamos a nuestra aplicación y veamos cómo funciona la pantalla táctil, porque como entiendes, la interfaz moderna implica interactividad, es decir, interacción con el usuario.



Aquí todo está organizado de manera bastante simple. Los eventos de los dispositivos de entrada se procesan en el ciclo del programa principal inmediatamente antes de renderizar la escena:



        /* Input */
        nk_input_begin(&rawfb->ctx);
        {
            switch (mouse->type) {
            case INPUT_DEV_MOUSE:
                handle_mouse(mouse, fb_info, rawfb);
                break;
            case INPUT_DEV_TOUCHSCREEN:
                handle_touchscreen(mouse, fb_info, rawfb);
                break;
            default:
                /* Unreachable */
                break;
            }
        }
        nk_input_end(&rawfb->ctx);


El mismo manejo de eventos desde la pantalla táctil ocurre en la función handle_touchscreen ():



handle_touchscreen
static void handle_touchscreen(struct input_dev *ts, struct fb_info *fb_info,
        struct rawfb_context *rawfb) {
    struct input_event ev;
    int type;
    static int x = 0, y = 0;

    while (0 <= input_dev_event(ts, &ev)) {
        type = ev.type & ~TS_EVENT_NEXT;

        switch (type) {
        case TS_TOUCH_1:
            x = normalize_coord((ev.value >> 16) & 0xffff, 0, fb_info->var.xres);
            y = normalize_coord(ev.value & 0xffff, 0, fb_info->var.yres);
            nk_input_button(&rawfb->ctx, NK_BUTTON_LEFT, x, y, 1);
            nk_input_motion(&rawfb->ctx, x, y);
            break;
        case TS_TOUCH_1_RELEASED:
            nk_input_button(&rawfb->ctx, NK_BUTTON_LEFT, x, y, 0);
            break;
        default:
            break;
        }

    }
}




De hecho, aquí es donde los eventos del dispositivo de entrada se convierten a un formato que Nuklear entiende. En realidad, eso es probablemente todo.



Lanzar en otro tablero



Habiendo obtenido resultados bastante decentes, decidimos reproducirlos en otra placa. Teníamos otra placa similar: STM32F769I-DISCO. Existe el mismo controlador LTDC, pero una pantalla diferente con una resolución de 800x480. Después de lanzarlo obtuvo 25 FPS. Es decir, una notable caída en el rendimiento. Esto se explica fácilmente por el tamaño del framebuffer: es casi 3 veces más grande. Pero el problema principal resultó ser diferente: la imagen estaba muy distorsionada, no había una imagen estática en el momento en que el widget debería estar en un solo lugar.



La razón no estaba clara, así que fuimos a ver ejemplos estándar de STM32Cube. Hubo un ejemplo con doble búfer para esta placa en particular. En este ejemplo, los desarrolladores, a diferencia del método con transparencia cambiante, simplemente mueven el puntero al framebuffer en la interrupción VBLANK. Ya hemos probado este método anteriormente para la primera placa, pero no funcionó. Pero al usar este método para STM32F769I-DISCO, obtuvimos un cambio de imagen bastante suave de 25 FPS.



Encantados, probamos este método nuevamente (con reordenamiento de punteros) en el primer tablero, pero aún así no funcionó a un FPS alto. Como resultado, el método con transparencias de capa (60 FPS) funciona en un tablero y el método con punteros de reordenamiento (25 FPS) en el otro. Después de discutir la situación, decidimos posponer la unificación hasta un estudio más profundo de la pila de gráficos.



Salir



Entonces, resumamos. El ejemplo que se muestra representa un patrón GUI simple pero común para microcontroladores: unos pocos botones, un control de volumen u otra cosa. El ejemplo carece de lógica asociada con eventos, ya que el énfasis se puso en los gráficos. En términos de rendimiento, obtuvimos un valor de FPS bastante decente.



Los matices acumulados para optimizar el rendimiento llevan a la conclusión de que los gráficos son cada vez más complicados en los microcontroladores modernos. Ahora, al igual que en las plataformas grandes, necesita monitorear el caché del procesador, colocar algo en la memoria externa y algo en una memoria más rápida, usar DMA, usar DMA2D, monitorear VBLANK, etc. Todo empezó a parecer una gran plataforma, y ​​tal vez por eso ya me he referido a X Server y Wayland varias veces.



Quizás una de las partes menos optimizadas es la propia representación, volvemos a dibujar toda la escena desde cero, por completo. No puedo decir cómo se hace en otras bibliotecas para microcontroladores, tal vez en algún lugar esta etapa esté integrada en la propia biblioteca. Pero en base a los resultados de trabajar con Nuklear, parece que en este lugar se necesita un análogo de X Server o Wayland, por supuesto, más ligero, lo que de nuevo nos lleva a la idea de que los sistemas pequeños siguen el camino de los grandes.



UPD1

Como resultado, el método para cambiar la transparencia no fue necesario. En ambas placas, funcionó un código común: intercambiar la dirección del búfer mediante v-sync. Además, el método con transparencias también es correcto, simplemente no es necesario.



UPD2

Quiero dar las gracias a todas las personas que sugirieron el almacenamiento en búfer triple, todavía no lo hemos conseguido. Pero ahora puedes ver que este es un método clásico (especialmente para FPS de alta velocidad de cuadros en la pantalla), que, entre otras cosas, nos permitirá deshacernos de los retrasos debidos a la espera de v-sync (es decir, cuando el software está notablemente por delante de la imagen). Aún no nos hemos encontrado con esto, pero es solo cuestión de tiempo. Y un agradecimiento especial por la discusión sobre el almacenamiento en búfer triple, quiero decirbesitzeruf y Belav!



Nuestros contactos:



Github: https://github.com/embox/embox

Newsletter: embox-ru [at] googlegroups.com

Telegram chat: t.me/embox_chat



All Articles