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:
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.
O mesmo conceito aplica-se aos gráficos. O Chromium hospeda um processo separado para operações relacionadas com a GPU.
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.
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.
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 paraCALayer
compartilhar entre processos;CALayerHost
- uma subclasseCALayer
que pode apresentar o conteúdo de uma camada remota;CAContextID
- um identificador globalmente único utilizado para identificar oCAContext
. Vale a pena mencionar que este token pode ser passado entre processos diretamente enquanto que a passagemmach_port
requer ações adicionais.
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.
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 classeCAOpenGLLayer
; - 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ânciaCALayerHost
.
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.
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.