Pretende desenvolver uma aplicação macOS com uma arquitetura multiprocessos em que uma imagem produzida em um processo deve ser apresentada em uma janela de outro processo? E se precisar apresentar não apenas uma imagem estática , mas um vídeo de tela cheia 4k com 60 fps? E gostaria de não queimar CPU do Mac ;) do seu cliente. Se isto é o que procura, este artigo é para você.

O meu nome é Kyrylo. Eu’tenho desenvolvido uma biblioteca comercial multiplataforma que permite integrar o controle do navegador Web Chromium em aplicações de ambiente de trabalho Java e .NET há muitos anos. Para integrar o Chromium numa aplicação de terceiros, temos de resolver uma série de objetivos desafiantes.

Uma das tarefas mais importantes e ambiciosas é a renderização. A sua complexidade está relacionada com a arquitetura multi-processos do Chromium. O Chromium processa o conteúdo de uma página Web em um processo separado da GPU e nós precisamos apresentar o conteúdo produzido em outro processo Java ou .NET. Este problema pode ser descrito da seguinte forma: apresentar o conteúdo gráfico de um processo  A em um processo  B:

Compartilhando o resultado da renderização com um processo distinto

Compartilhando o resultado da renderização com um processo distinto

Esta tarefa é resolvida de forma diferente em cada plataforma suportada. Por exemplo, no Windows e no Linux, isto é feito através da incorporação da janela Chromium na janela da aplicação de destino. Este é um caso de utilização comum para as funções da API nativa: Win32 API’ SetParent ou XLib’ ReparentWindow.

Infelizmente, esta abordagem não funciona no macOS. É proibido incorporar uma janela de um processo numa janela de outro processo. No entanto, há formas de resolver esta questão.

Neste artigo, mostrarei como renderizar o conteúdo criado em um processo em uma janela em outro processo no macOS utilizando a abordagem de partilha CALayer. O conceito será ilustrado por uma aplicação simples.

Arquitetura de multiprocessamento do Chromium 

Nesta seção, vamos rever brevemente a arquitetura do Chromium.

A ideia principal do Chromium é utilizar processos separados para a apresentação de páginas Web. Cada processo é isolado dos outros e do resto do sistema.

Na figura abaixo, o Browser representa o processo principal (janela de nível superior). Renderer é responsável pela interpretação e apresentação do HTML (aba do browser).

Isto é feito por razões de segurança e torna o Chromium semelhante ao conceito de sistema operacional: uma falha numa aplicação (GPU, processo de renderização, etc.) não falha o sistema global.

Arquitetura multi-processo do Chromium

Arquitetura multi-processos do Chromium (retirado daqui)

O mesmo conceito aplica-se aos gráficos. O Chromium hospeda um processo separado para operações relacionadas com a GPU.

Processo GPU do Chromium

Processo GPU do Chromium

Compartilhamento de texturas 

Voltemos à tarefa original de obter pixels de um processo distinto. Antes de mergulhar nos pormenores de implementação, é essencial compreender que entidades são utilizadas para a renderização no macOS.

Um modelo CoreAnimation simplificado

Um modelo CoreAnimation simplificado

Na figura acima, pode ver os principais objetos utilizados para apresentar o conteúdo do gráfico e as relações entre eles:

  • NSWindow representa uma janela na tela. Esta classe fornece uma área para incorporar outras views, aceita e distribui eventos causados pela interação do usuário através de um mouse e de um teclado;
  • UIView - um bloco de construção fundamental que processa o conteúdo dentro dos limites do conteúdo . Trata-se de uma classe de base para os controles da IU, tais como etiquetas, botões, cursores;
  • CALayer - um objeto utilizado para fornecer o armazenamento de apoio para as views. As instâncias CALayer também podem ser utilizadas para apresentar conteúdo visual sem uma view principal. As camadas são aplicadas para a personalização da view, por exemplo, adicionando raios, sombras, realces, etc.

Estamos interessados em  CALayer porque  proporciona mais flexibilidade para a apresentação de conteúdos. Para sermos exatos, trabalharemos com a sua subclasse  — CALayerHost, que é essencial para o compartilhamento de texturas. Então o que é “compartilhamento de textura”?

O compartilhamento de texturas pode ser descrito como um processo de utilização de conteúdos gráficos criados num processo com outro processo. Esta é uma implementação do conceito de multi-processo descrito neste artigo.

Criação de conteúdos gráficos

Criação de conteúdos gráficos num processo — apresentação num processo diferente

IOSurface 

A Apple fornece meios para o objetivo descrito. É a estrutura IOSurface. Esta API é baseada no objeto IOSurface que pode ser acessado a partir de um processo remoto através das primitivas de baixo nível do kernel do macOS denominadas mach_ports. A abordagem IOSurface é utilizada no Chromium e nós as utilizamos há muito tempo  no nosso projeto. Tem um bom desempenho e uma API estável. No entanto, após algum tempo, nos deparamos com um problema em que alguns sites não são apresentados corretamente quando se ativa o fluxo de renderização IOSurface no Chromium. Isto nos levou a investigar a abordagem CALayerHost que é a predefinida em Chromium no macOS.

CALayerHost 

Como já referi, CALayerHost é a subclasse de CALayer que processa o contexto de processamento de outra camada. O compartilhamento de texturas através de CALayerHost envolve as seguintes entidades:

  • CAContext - um objeto CoreAnimation que representa as informações sobre o ambiente e é utilizado para CALayer compartilhar entre processos;
  • CALayerHost - uma subclasse CALayer que pode apresentar o conteúdo de uma camada remota;
  • CAContextID - um identificador globalmente único utilizado para identificar o CAContext. Vale a pena mencionar que este token pode ser passado entre processos diretamente enquanto que a passagem mach_port requer ações adicionais.

Classes necessárias para o compartilhamento de renderização

Classes necessárias para o compartilhamento de renderização

Exemplo 

Vou ilustrar a ideia do compartilhamento de texturas com uma aplicação simples. Esta aplicação será semelhante à estrutura do Chromium, ou seja, processos separados para objetivos diferentes.

O processo da GPU fará todas as coisas de desenho usando CALayer como um buffer de retorno. Depois de desenhar o CAContextID deste CALayer será enviado para o processo principal. O processo principal utilizará CAContextID para criar objetos CALayerHost para apresentar o conteúdo nos objetos NSWindow criados.

Pormenores da implementação 

Esta aplicação é composta por dois processos:

  • Renderer App - responsável pela criação da textura a ser compartilhada;
  • Host App - apresenta o conteúdo criado na aplicação Renderer.

A comunicação entre os processos é efetuada através de uma biblioteca IPC simples.

Exemplo da arquitetura da aplicação

A arquitetura da aplicação de exemplo

A estrutura acima é uma velha e boa abordagem cliente-servidor, em que a Aplicação Host efetua os pedidos correspondentes à Aplicação Renderer.

Vejamos as partes importantes da aplicação.

A função principal inicia posteriormente ambos os processos. O número de texturas a renderizar é configurado por uma variável constante:

int main(int argc, char* argv[]) {
  constexpr int kNumberWindows = 2;
  if (ipc::Ipc::instance().fork_process()) {
    HostApp server_app;
    server_app.run(kNumberWindows);
  } else {
    RendererApp client_app;
    client_app.run(kNumberWindows);
  }
}

Declarar a API necessária 

A classe CALayerHost, bem como outros objetos necessários para o compartilhamento de camadas, é uma API runtime. Assim, deve ser declarado explicitamente no nosso código.

// O CGSConnectionID é usado para criar o CAContext no processo
// que vai compartilhar os CALayers que ele está renderizando    
// para outro processo exibir.
extern "C" {
  typedef uint32_t CGSConnectionID;
  CGSConnectionID CGSMainConnectionID(void);
};

// O tipo CAContextID identifica um CAContext entre processos.    
// Este é o token que é passado do processo que está compartilhando
// o CALayer que está renderizando para o processo que irá
// mostrar esse CALayer.
typedef uint32_t CAContextID;

// O CAContext tem um CAContextID estático que pode ser enviado para 
// outro processo. Quando um CALayerHost é criado usando esse 
// CAContextID em outro processo, o conteúdo exibido por esse 
// CALayerHost será o conteúdo do CALayer que é definido como a propriedade // layer| no CAContext.
@interface CAContext : NSObject
  + (id)contextWithCGSConnection:(CAContextID)contextId options:                           
                                 (NSDictionary*)optionsDict;
  @property(readonly) CAContextID contextId;
  @property(retain) CALayer *layer;
@end

// O CALayerHost é criado no processo que irá mostrar o 
// conteúdo que está a ser processado por outro processo. Definir a propriedade 
// |contextId| num objeto desta classe fará com que esta camada 
// mostre o conteúdo da CALayer que está definida para o 
// CAContext com esse CAContextID no processo de compartilhamento de camadas.
@interface CALayerHost : CALayer
  @property CAContextID contextId;
@end

Renderer App 

A aplicação Renderer executa duas tarefas principais:

  • Renderiza conteúdo por meio da API OpenGL. Isto é feito na classe ClientGlLayer que herda da classe CAOpenGLLayer;
  • Expõe a camada recém-criada à aplicação anfitriã __ através do CAContext e da biblioteca IPC.
void RendererApp::exportLayer(CALayer* gl_layer) {
  NSDictionary* dict = [[NSDictionary alloc] init];
  CGSConnectionID connection_id = CGSMainConnectionID();
  CAContext* remoteContext = [CAContext                                                                           

  contextWithCGSConnection:connection_id options:dict];

  printf("Renderer: Definindo a camada do CAContext para o CALayer                                                                                            
                                                    para exportar.\n");
  [remoteContext setLayer:gl_layer];

  printf("Renderizador: A enviar o ID do contexto de volta para o servidor                                                                                                                                        
                                                       .\n");
  CAContextID contextId = [remoteContext contextId];
  // Enviar o contextId para a HostApp.
  ipc::Ipc::instance().write_data(&contextId);
}

O código de compartilhamento do CALayer é simples:

  • Inicializar o CAContext;
  • Expor a camada para exportação através do método CAContext::setLayer();
  • Passa o identificador de CAContext para a aplicação Renderer através da biblioteca IPC. Aí ele será utilizado para criar uma instância CALayerHost.

Host App 

O principal objetivo da Host App é apresentar o conteúdo apresentado na Renderer App. Cada camada é servida por uma janela separada NSWindow. Em primeiro lugar, precisamos inicializar um CALayerHost com um CAContextID recebido do processo Renderer App:

CALayerHost*HostApp::getLayerHost() {
  if (context_id_ == 0) {                                
    ipc::Ipc::instance().read_data(&context_id_);
  }
  CALayerHost* layer_host = [[CALayerHost alloc] init];
  [layer_host setContextId:context_id_];
  return layer_host;
}

Por uma questão de conveniência, guardaremos o ID recebido no campo context_id . Em seguida, o CALayerHost deve ser incorporado no NSView de uma nova janela criada:

[window setTitle:@"Exemplo de CARemoteLayer"];
[window makeKeyAndOrderFront:nil];

NSView* view = [window contentView];
[view setWantsLayer:YES];

CALayerHost* layer_host = getLayerHost();

[[[window contentView] layer] addSublayer:layer_host];
[layer_host setPosition:CGPointMake(240, 240)];
printf("Host: Adicionada a camada à hierarquia da vista.\n");

Importante: a propriedade setWantsLayer deve ser definida como YES, para que a view utilize uma CALayer para gerir o seu conteúdo processado.

Abaixo pode ver o resultado da aplicação para duas janelas. Note-se que cada janela utiliza a mesma CALayer na Render App.

Executar a aplicação com duas janelas

Executar a aplicação com duas janelas

Pode encontrar o código fonte completo da aplicação de exemplo com a instrução de compilação aqui.

Conclusão 

Colocar a renderização da GPU num processo separado é uma abordagem popular usada em um projeto tão grande como o Chromium. Isto melhora a segurança global do produto e melhora a facilidade de manutenção do código. Neste artigo, testamos a abordagem baseada no compartilhamento CALayer. A ideia principal do tópico descrito é ter um processo que efetua o desenho em um CALayer e compartilha a camada final com outro(s) processo(s) que pode(m) apresentar as camadas renderizadas nas views de destino.