Shader OSL Multithread

Essas últimas semanas eu estava tentando implementar suporte a múltiplas threads do shader OSL. O BRL-CAD usa pthreads para paralelizar o trabalho.

A primeira vez que eu pus o shader OSL pra rodar, tive problemas, provavelmente devido à leitura e escritas simultâneas a dados na memória. Eu tinha adiado esse problema e estava trabalhando apenas com uma thread.

O que a minha função osl_render faz é preencher uma estrutura chamada RenderInfo com dados como ponto de interseção, a normal, um ponteiro para o shader dessa superfície, etc. Preenchida essa estrutura, ela faz uma chamada ao método QueryColor da classe OSLRender. O objeto dessa classe é global, o que significa que está em uma posição de memória compartilhada pelas threads. Como a chamada do método QueryColor modifica o objeto, temos o problema do acesso simultâneo a esse método.

Primeira tentativa

A primeira coisa que eu tentei, foi proteger a chamada dessa função com semáforos. O BRL-CAD tem uma biblioteca interna de utilitários, entre eles a implementação de um semáforo.

Basicamente, o que eu fiz foi o seguinte:

OSLRender oslr;
...
bu_semaphore_acquire(BU_SEM_SYSCALL);
oslr.QueryColor();
bu_semaphore_release(BU_SEM_SYSCALL);

BU_SEM_SYSCALL na verdade é uma macro representando um inteiro. Quando uma thread chama a função bu semaphore acquire(BU_SEM_SYSCALL), ela bloqueia o acesso a outras threads para o mesmo inteiro.

Com isso o código passou a funcionar para mais de uma thread! Porém, fui conservador demais ao bloquear a chamada do QueryColor. Creio que 90% do trabalho da função osl_render seja feito lá. Logo, colocando um semáforo nessa função, a paralelização vai por água abaixo. De fato, fiz testes com 1 e 2 processadores e os tempos para renderizar uma imagem de teste foram exatamente os mesmos.

Segunda tentativa

Fui estudar um pouco mais o código do OSL e descobri que ele tem suporte a aplicativos multithread. Basicamente, cada thread deve manter uma estrutura local chamada ThreadInfo. Ao chamar o sistema de shaders do OSL, passamos essa informação, que será modificada.

Gambiarra!

Para manter uma ThreadInfo para cada thread, precisamos identificá-las de alguma forma. O problema é que não temos acsso a essa informação diretamente. O único jeito que encontrei sem ter que mudar a estrutura interna do raytracer do BRL-CAD foi a seguinte: toda vez que a função osl_render é chamada, uma estrutura chamada struct application é passada. Essa estrutura tem um campo chamado resource, que é um ponteiro para um pedaço de memória exclusivo para cada thread.

Esse pedaço é alocado na criação das threads e não muda ao longo de suas vidas. Assim, usar o endereço desse pedaço de memória é um jeito (feio) de identificá-las. Outro problema é que, embora haja uma função de inicialização do shader (chamado osl_setup), as threads são criadas após isso, o que me obriga a inicializá-las na função osl_render mesmo.

A solução na qual cheguei foi a seguinte:

/* Every time a thread reaches osl_render for the 
   first time, we save the address of their own
   buffers, which is an ugly way to identify them */
std::vector<struct resource *> visited_addrs;
/* Holds information about the context necessary to
   correctly execute a shader */
std::vector<void *> thread_infos;

int osl_render(struct application *ap, ...){
    ...
    bu_semaphore_acquire(BU_SEM_SYSCALL);
    
    /* Check if it is the first time this thread is
       calling this function */
    bool visited = false;
    for(size_t i = 0; i < visited_addrs.size(); i++){
        if(ap->a_resource == visited_addrs[i]){
            visited = true;
            thread_info = thread_infos[i];
            break;
        }
    } 
    if(!visited){
        visited_addrs.push_back(ap->a_resource);
        /* Get thread specific information from 
           OSLRender system */
        thread_info = oslr->CreateThreadInfo();
        thread_infos.push_back(thread_info);
    }
    bu_semaphore_release(BU_SEM_SYSCALL);
    ...
}

Classe thread-safe

Com as ThreadInfo's, podemos nos despreocupar com o acesso concorrente ao objeto da classe OSLRender. Para ter uma garantia de que não será feita escrita nesse objeto, podemos declarar os métodos como const, como no exemplo abaixo:

struct A {
    int x;
    // OK
    void set_x(int _x) { x = _x; };
    // ERRO, tentando modificar membro do objeto 
    // num método const
    void const_set_x(int _x) const { x = _x; };
};

Testes

Com essa nova implementação, os tempos melhoraram bastante com mais processadores. A tabela abaixo mostra os tempos de execução para renderizar uma cena de exemplo, usando até 4 processadores.

Gráfico 1: Tempo x no. de processadores

Os ganhos foram praticamente lineares, sendo que para 4 processadores a medida de paralelização foi de 1.25 (se fosse perfeitamente linear seria 1.0).

Próximos passsos

Falei com meu mentor sobre o desenvolvimento daquele modo de visualização incremental, mas parece que ele não gostou muito da ideia. Isso exigiria uma mudança bastante grande no sistema, pois o renderizador do BRL-CAD foi desenvolvido para ray-tracing e não path-tracing.

Por enquanto estou estudando maneiras de adaptar o código OSL para suportar ray-tracing, mas não sei se isso é viável. A propósito, eu ainda confundo bastante os termos ray-tracing, path-tracing, photon mapping e todos esses algoritmos de iluminação e pretendo em breve escrever um post cobrindo esses tópicos bem por cima.

Os comentários estão fechados.

%d bloggers like this: