Stage 2: el frame nunca sale de la GPU
Una semana después, el viaje de ida y vuelta por la CPU ha desaparecido. No optimizado — desaparecido.
Servo sigue pintando en su propio framebuffer offscreen, a través de un contexto de render que nuestro fork construye a partir del handle de display del host con una superficie que nadie compone, de modo que GPUI sigue siendo dueño del drawable que la pantalla muestra de verdad. Lo que ha cambiado es lo que viene después. Cuando Servo da un frame por listo, tick() le pide a ese contexto que exporte el framebuffer y recibe a cambio un handle: un descriptor de fichero dma-buf de la memoria de la GPU, y un fence que dice cuándo se han completado las escrituras. La propiedad de ambos pasa a ser nuestra. El handle va a un slot compartido con el lado de GPUI.
La otra mitad vive en el backend wgpu de GPUI. draw_external_sprites recoge el handle, importa el dma-buf como un wgpu::Texture a través de wgpu::hal::vulkan y ash, y lo dibuja como un sprite más de la escena. Para GPUI, la página web es una textura como cualquier otra. El camino de Stage 1 — el readback, el intercambio rojo/azul, una subida de RenderImage en cada frame — está borrado, no desactivado. Ya no rebota nada por la memoria del sistema.
El fence es la parte delicada. Un sync fd es de un solo uso: en cuanto alguien ha esperado sobre él, volver a esperar no tiene un significado definido. Así que se lo entregamos al primer consumidor del frame y dejamos que el resto recurra a una espera más gruesa. El handle dma-buf sí lo mantenemos en caché, y por eso un frame en el que Servo no pintó nada sigue mostrando el último bueno en vez de parpadear.
El reenvío de ratón y scroll entró el mismo día. Ahí fue cuando la página respondió a un clic por primera vez. Hasta entonces era una captura de pantalla carísima que encima se actualizaba.