Fase 1: píxels a la pantalla
Què havia de demostrar el shell
L'11 de maig del 2026 el repositori encara es deia kompass. Aquell mateix vespre va passar a dir-se rumb: Kompass International SA té la marca registrada en les classes que cobreixen el programari, així que el nom no podia servir com a marca pública. Rumb és, en català, el terme nàutic de la direcció que marca la brúixola, i manté el tema de la rosa dels vents que els forks ja tenien — tramuntana, migjorn, ponent. El commit del canvi de nom va tocar el nom del crate, el binari, l'struct de l'aplicació i el títol de la finestra, i res més.
La pregunta que el codi havia de respondre aquell dia era més estreta que «podem fer un navegador?». Era: poden conviure en un mateix procés un WebView de Servo i una aplicació de GPUI? Tots dos són renderitzadors. Tots dos esperen governar un bucle de fotogrames. Tots dos volen un context de GPU. L'API d'incrustació de Servo està escrita pensant en qui la incrusta com a propietari d'una finestra i d'un compositor; GPUI està escrit donant per fet que ell és el propietari de la finestra i del compositor. La fase 1 és la resposta més petita possible a què passa quan els demanes que tots dos s'equivoquin en això.
Si no poden compartir procés sense que l'un li prengui la finestra a l'altre, no hi ha projecte — i la manera més barata de saber-ho és connectar-los pel camí més tosc que doni una imatge, i després mirar-se la imatge. La fase 1 és aquest camí.
Dos renderitzadors, una finestra
Servo es crea amb WindowRenderingContext::new_offscreen_only, un afegit del nostre fork de Servo. Construeix un context de surfman de GPU real a partir del handle de display de l'amfitrió — renderitzat per maquinari de debò, amb WebRender i tot — però li dona una superfície Generic: una superfície que ningú no presenta, ni redimensiona, ni compon.
Aquesta sola decisió és el que fa possible tota la resta. GPUI continua sent propietari del drawable que la pantalla mostra de debò. Servo pinta en un framebuffer que només existeix dins del procés. Cap de les dues bandes no ha de negociar per la finestra, perquè només una la toca mai. Tot el que s'ha construït des d'aleshores es recolza en aquesta separació — l'exportació dma-buf una setmana després, un framebuffer per pestanya sis setmanes més tard.
Hi ha una preferència de Servo que s'activa a la força en construir-lo: WebGL2, que ve desactivada per defecte. Sense ella, three.js i qualsevol altre web amb WebGL2 no renderitzen res, i la diferència entre «hem dibuixat una pàgina» i «hem dibuixat una pàgina amb una escena 3D a dins» era bona part del que la prova havia de comprovar.
Llegir, intercanviar, pujar
El camí del fotograma era tan literal com sona. A cada tick de render: fer girar el bucle d'esdeveniments de Servo, dir al WebView que pinti i després cridar read_to_image sobre el framebuffer fora de pantalla. Això treu el fotograma acabat de la memòria de la GPU i el porta a la memòria del sistema com un buffer d'imatge corrent.
Servo el lliura en RGBA. GPUI puja els framebuffers en BGRA8. Així que el shell recorria el buffer i intercanviava el vermell i el blau, píxel a píxel, fotograma a fotograma, i passava el resultat a paint_image com si fos un PNG que algú hagués carregat del disc.
Compta les passades sobre el framebuffer i en surten tres: la lectura des de la memòria de la GPU, l'intercanvi passant per la CPU i la pujada de tornada. Dues hi són només perquè les dues meitats de la pila no es posen d'acord sobre l'ordre dels bytes. Totes tres hi són perquè encara no hi ha manera que apuntin a la mateixa memòria.
Res d'això no és enginyós ni ho pretenia ser. És el primer moviment estàndard quan has de fer de pont entre dos renderitzadors que no comparteixen textura: converteix els píxels en bytes, perquè els bytes van a tot arreu.
On se n'anava el temps
Una anada i tornada completa per la CPU, un cop per fotograma, per a uns píxels que no havien sortit mai de la targeta — i el cost escala amb l'àrea del viewport, de manera que el camí empitjora just quan la finestra comença a valer la pena.
El ritme encara ho agreujava. El bucle només el movia request_animation_frame; no hi havia cap camí que anés de «Servo ha acabat un fotograma» a «GPUI, dibuixa ara». Les actualitzacions a pantalla es quedaven pels volts d'1 Hz.
Aquesta xifra és una observació anotada al commit, no una mesura. No hi ha cap perfil, cap traça de temps per fotograma, cap execució de comparació, i no n'hi ha hagut mai cap. No vam mesurar el readback perquè no teníem intenció de conservar-lo — el commit que el va introduir ja hi deixava escrita la condició de sortida: la fase 2 necessita una textura compartida de GL a WGPU de debò.
Què no va resoldre
Gairebé tot.
El redimensionament de la finestra no estava connectat al motor. ServoEngine es construïa amb una mida fixa i mai no se li deia que n'hi havia una de nova, així que la pàgina es maquetava un sol cop amb les dimensions inicials i GPUI estirava el resultat fins a la mida que acabés tenint la finestra. Les media queries de CSS s'avaluaven contra el viewport equivocat, per sempre.
A la pàgina no hi arribava cap entrada. Ni clics, ni scroll, ni tecles, ni el pas del cursor per sobre. Els esdeveniments de GPUI queien sobre l'element d'imatge i s'hi aturaven. L'apunt de backlog que es va obrir per això ho diu sense embuts: la pàgina es renderitza però és de només lectura, i l'entrada és un subsistema a part que necessita els seus propis commits.
I el build existia exactament en una màquina. tramuntana, ponent i stylo eren tots dependències per ruta que apuntaven a checkouts germans. Ningú més no hauria pogut compilar això.
El que sí que tenia era una pàgina a la pantalla, dibuixada per Servo, dins d'una finestra de GPUI. Aquesta era tota la pregunta, i la resposta era sí. La pregunta següent era millor.